Problem/Motivation

Currently, any inline styles in HTML get stripped out when editing content for text-formats that use both the core CKEditor and any output filtering. This includes simple and generally useful styles like <td style="width: 25%">. #2099741: Protect WYSIWYG Editors from XSS Without Destroying User Data put this filtering in place to prevent XSS attacks via saved content being rendered by an editor. While this brings good safety, the current blanket prohibition on any inline styles while CKEditor with output filtering is used causes issues such as #2785483: [upstream] Cannot set cell width or height because inline styles are stripped and #3073961: allowed styles are removed upon editing in ckeditor.

#2099741: Protect WYSIWYG Editors from XSS Without Destroying User Data and the Text editor XSS filtering documentation indicate that editors can override the XSS filter class used and provide their own implementation. Unfortunately, extending \Drupal\editor\EditorXssFilter\Standard to add filtering of style attributes requires overriding the large and complex \Drupal\Component\Utility\Xss::attributes() function.

Proposed resolution

I would like to propose refactoring \Drupal\Component\Utility\Xss::attributes() to make the code more understandable as well as make it easier for child classes to add support for filtering of inline style attributes while continuing to use the default filtering for all other attributes.

Remaining tasks

I'll follow up with a proof-of-concept refactoring of \Drupal\Component\Utility\Xss::attributes() that maintains existing behavior by default, but is easier to extend for this purpose or related purposes.

Needs test coverage.

User interface changes

N/A

API changes

TBD

Data model changes

TBD

Release notes snippet

TBD

CommentFileSizeAuthor
#92 3109650-92-xss-attributes--10.6.x.patch11.87 KBlexbritvin
#92 3109650-92-xss-attributes--main.patch11.88 KBlexbritvin
#90 core-10.2.3-xss-refactor_filter_attributes-3109650-74--reroll-10.5.6.patch4.98 KBjaroslav červený
#85 xss-refactor-filter-attributes-10.5.x-3109650-85.patch3.72 KBarantxio
#79 xss-refactor-filter-attributes-11.1.7-3109650-77.patch3.71 KBnadeemkmir
#78 xss-refactor-filter-attributes-11.0.x-3109650-76.patch3.68 KBluongosb
#75 xss-refactor-filter-attributes-10.4.x-3109650-75.patch3.68 KBhmdnawaz
#74 interdiff_68-74.txt1.64 KBparamnida
#74 core-10.2.3-xss-refactor_filter_attributes-3109650-74.patch4.96 KBparamnida
#72 drupal-n3109650-72-102x.patch3.68 KBdamienmckenna
#71 3109650-71.patch3.67 KBquentin.le-delas
#70 3109650-refactor-xss-attributes.patch3.67 KBquentin.le-delas
#69 3109650-refactor-xss-attributes.patch3.9 KBquentin.le-delas
#68 core-10.1-xss-refactor_filter_attributes-3109650-68.patch4.96 KBbgreco
#64 3109650-64.patch3.67 KBs3b0un3t
#63 3109650-63.patch3.07 KBnadim hossain
#62 3109650-62.patch3.07 KBeugene.brit
#60 3109650-60.patch2.98 KB_utsavsharma
#60 interdiff_59-60.txt483 bytes_utsavsharma
#59 3109650-9.5.x-59.patch2.97 KBruslan piskarov
#59 3109650-9.4.x-59.patch2.94 KBruslan piskarov
#58 3109650-58.patch2.97 KBruslan piskarov
#56 reroll_diff_53-56.txt2.47 KBravi.shankar
#56 3109650-56.patch2.94 KBravi.shankar
#53 core-xss-attribute-alter-hook-3109650-53.patch2.88 KBlobodakyrylo
#52 core-xss-attribute-alter-hook-3109650-52.patch2.87 KBjohnpitcairn
#44 core-8.9-xss-refactor_filter_attributes-3109650-44.patch5.5 KBtechnoveltyco
#43 core-8.9-xss-refactor_filter_attributes-3109650-43.patch2.59 KBtechnoveltyco
#43 core-9.1-xss-refactor_filter_attributes-3109650-43.patch5.49 KBtechnoveltyco
#43 core-9.2-xss-refactor_filter_attributes-3109650-43.patch5.49 KBtechnoveltyco
#43 Drupal 9.2 XSS style filter - Table style test - Screenshot 2021-09-22 at 11.21.50.png177.12 KBtechnoveltyco
#42 refactor_xss_attributes-3109650-42.patch7.37 KBjoshua1234511
#41 refactor_xss_attributes-3109650-41.patch7.68 KBjoshua1234511
#40 refactor_xss_attributes-3109650-40.patch7.32 KBjoshua1234511
#38 refactor_xss_attributes-3109650-38.patch1.76 KBloziju
#38 interdiff_36-38.txt5.77 KBloziju
#36 interdiff-3109650-33_36.txt638 bytesgauravvvv
#36 3109650-36.patch7.32 KBgauravvvv
#33 refactor_xss_attributes-3109650-33.patch7.32 KBbgreco
#27 refactor_xss_attributes-3109650-27.patch39.64 KBnitesh sethia
#26 refactor_xss_attributes-3109650-26.patch39.65 KBnitesh sethia
#24 refactor_xss_attributes-3109650-24.patch39.71 KBadamfranco
#23 refactor_xss_attributes-3109650-23.patch7.28 KBadamfranco
#22 3109650-d8-22.patch7.12 KBabhijith s
#17 refactor_xss_attributes-3109650-17.patch7.1 KBu7aro
#14 cke-filter.png46.22 KBelgandoz
#12 Screenshot 2020-09-04 at 20.59.04.png204.8 KBras-ben
#9 refactor_xss_attributes-3109650.patch6.91 KBras-ben
#3 refactor_xss_attributes-3109650-3.patch39.54 KBadamfranco
#2 refactor_xss_attributes-3109650-2.patch5.97 KBadamfranco

Issue fork drupal-3109650

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

adamfranco created an issue. See original summary.

adamfranco’s picture

StatusFileSize
new5.97 KB

This patch breaks apart \Drupal\Component\Utility\Xss::attributes() into several methods which can then be overridden by child classes.

It also includes a \Drupal\editor\EditorXssFilter\InlineStyles sub-class that (I believe) can safely allow basic style values in HTML. Rather than trying to exhaustively cover all possible style values, the implementation in this patch only allows simple values like:

  • width: 50%
  • color: #ac45ff
  • background-color: blue
  • margin-top: 3px
  • display: flex

It does this by limiting the allowed characters in the value to a-z0-9_,%#\.-. Any complex transforms or URLs are disallowed by the lack of parenthesis. Disallowing parentheses, asterisks, escape characters, and quotes limits available mechanisms for XSS injection.

Note that this patch includes only the supporting Xss refactoring and tests but not the additional code needed to get the \Drupal\editor\EditorXssFilter\InlineStyles class to be used when editing with CKEditor.

adamfranco’s picture

StatusFileSize
new39.54 KB

My patch in #2 didn't include the new files. Re-rolling.

adamfranco’s picture

In case anyone wishes to use the new InlineStyles editor-XSS-filter provided in patch #3 prior to any incorporation or changes of the defaults, you can add the following to a module after applying patch #3:


use Drupal\filter\FilterFormatInterface;

/**
 * Implements hook_editor_xss_filter_alter().
 */
function mymodule_editor_xss_filter_alter(&$editor_xss_filter_class, FilterFormatInterface $format, FilterFormatInterface $original_format = NULL) {
  // Use the InlineStyles XSS filter to allow basic styles to be retained in the
  // editor while still filtering for XSS exploits.
  //
  // The class referenced is provided by a patch from:
  // https://www.drupal.org/project/drupal/issues/3109650
  $editor_xss_filter_class = '\\Drupal\\editor\\EditorXssFilter\\InlineStyles';
}

adamfranco’s picture

Status: Active » Needs review
fabianderijk’s picture

Unfortunatly the patch doesn't do anything. After applying the patch and the hook in #4 the style attribute is still being removed.

bgilhome’s picture

@fabianderijk the "Limit allowed HTML" filter (Drupal\filter\Plugin\Filter\FilterHtml) is the culprit here - it uses Xss::filter() which AFAICT doesn't use the refactored methods. Line 87 of core/Drupal/Component/Utility/Xss.php strips out the style attribute here.

Should that also be refactored, and we could implement an alter hook in Drupal\filter\Plugin\Filter\FilterHtml to override the class? Or should it all be done in the latter?

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

ras-ben’s picture

StatusFileSize
new6.91 KB

I couldnt get the patch and the custom hook to work either.
The documentation for the hook says:

"Is only called when an EditorXssFilter will effectively be used; this hook does not allow one to alter that decision."

So i assume that is part of the problem?

I've adjusted the patch to make it a little.. simpler.
You dont need any custom hooks to get this patch to work, BUT BE WARNED:

This will allow style="" on every text-formatter. If you have any Wysiwyg editors that are open to non-trusted sources, you should NOT add this patch, as it would introduce a XSS security issue!

Status: Needs review » Needs work

The last submitted patch, 9: refactor_xss_attributes-3109650.patch, failed testing. View results

elgandoz’s picture

Patch #9 kinda works but I still have an issue in the rendered markup:
I'm able to use the CKeditor cell alignment (<td style="text-align:right">), which is applied and visible while editing, but the style attribute disappear on rendering (when I view the node). If I re-edit the node, the alignment is still present and inspecting the code (or using CKE "Source") correctly displays the style attribute, meaning it gets saved.

The big question: Is there am extra XSS filtering upon rendering? If yes, how can I avoid it? (possibly in the less disruptive and most secure way...)

ras-ben’s picture

StatusFileSize
new204.8 KB

@elgandoz You also need to allow "style" for the tags

-

/admin/config/content/formats/manage/basic_html

elgandoz’s picture

@ras-ben , thanks for your prompt answer, but the style attribute is already there. If it wasn't, I wouldn't even be able to select the alignments (or other) options from the CKEditor table properties dialog.

The difference here is that before the patch, the style attribute was stripped upon saving (coming back to the editing form the inline-style was gone), while now they are saved but removed on the rendering only.

I will also try this on a vanilla setup in order to confirm.

EDIT: I found what's causing this problem: I am using attributes values (<td rowspan colspan id headers style="text-align">). The key here is style="text-align" instead of just style.

From the "Allowed HTML tags" help text:

A list of HTML tags that can be used. By default only the lang and dir attributes are allowed for all HTML tags. Each HTML tag may have attributes which are treated as allowed attribute names for that HTML tag. Each attribute may allow all values, or only allow specific values. Attribute names or values may be written as a prefix and wildcard like jump-*. JavaScript event attributes, JavaScript URLs, and CSS are always stripped.

CKEditor receives the available options for its dialogs from the "Allowed HTML tags" list: the difference here is that if I leave just styles it adds a lot of options to the table cell properties that I explicitly don't want the authors to use (see below screenshots).

cke filter issue

Listed here are the different cases:

  1. If I add only style all the possible options are included in the dialog, Drupal saves and render the inline styles
  2. If I add the style="text-align" the options are reduced (as expected), Drupal saves correctly the data but it doesn't render the inline style
  3. If I add the style="text-align:right" interestingly the options are completely removed (CKE doesn't recognize them), but if the inline styles are already saved in the markup, Drupal renders it correctly

Basically in case we define attributes values, the rendering and CKE options are mis-aligned.

Despite it may seem very specific to my case, I reckon this could affect all the other CKE widgets/plugins; in my specific case I was using the tables one.

In a nutshell: patch #9 works only if you want to allow all styles and you don't mind adding all the options in CKE dialogs.
It currently works as a workaround but it should retain the CKEditor compatibility if we want to fix this in core.

I'll try to see if there's a way to rectify this.

elgandoz’s picture

StatusFileSize
new46.22 KB
johan den hollander’s picture

#9 works for me as I wanted to offer all options for tables to the editor.

ras-ben’s picture

Just thought i'd re-add my comment before, as it's VERY important to note:

If you have any Wysiwyg editors that are open to non-trusted editors/users, you should NOT add this patch, as it would introduce a XSS vulnerability!

u7aro’s picture

StatusFileSize
new7.1 KB

This is a #9 patch that supports Drupal 9.1.

ckhalilo’s picture

This patch work fo me, but still have question, did that patch Patch 17 not introduce XSS vulnerability!!!

krzysztof domański’s picture

@ckhalilo The previous #9 and #17 (re-roll) patches are the same. In comments #9 and #16 the risk is mentioned.

ckhalilo’s picture

Thank you

smustgrave’s picture

Question would this issue be why certain classes of mine get stripped out? A library I have to use has classes like mobile-lg:grid-col-3 and they get removed while others remain.

abhijith s’s picture

StatusFileSize
new7.12 KB
adamfranco’s picture

StatusFileSize
new7.28 KB

Re-Roll #22 to not conflict with the SA-CORE-2021-002 change in 8.9.14.

adamfranco’s picture

StatusFileSize
new39.71 KB

Here is a re-roll #3 to not conflict with the SA-CORE-2021-002 change in 8.9.14.

Note that unlike the intermediate set of patches in #9, #17, #22, and #23, this patch does not apply to all text-editor areas by default and is safe to apply by default. To allow inline style attributes you will need to implement hook_editor_xss_filter_alter() to use the \Drupal\editor\EditorXssFilter\InlineStyles sub-class as described in #4.

johan den hollander’s picture

Applied the #23 without a problem. Thanks!

nitesh sethia’s picture

Version: 8.9.x-dev » 9.1.x-dev
StatusFileSize
new39.65 KB

Creating a new patch as per Drupal 9.

nitesh sethia’s picture

StatusFileSize
new39.64 KB

Fixing the issue with $value variable.

nitesh sethia’s picture

Status: Needs work » Needs review

The last submitted patch, 23: refactor_xss_attributes-3109650-23.patch, failed testing. View results

drumanuel’s picture

patched 8.9.14 with 24 without problems

madhan mohan’s picture

@Drumanuel is the patch applied to drupal version 8.9.14?

piotrkonefal’s picture

For some reason hook_editor_xss_filter_alter() is not firing anymore, so there is no way to use the '\\Drupal\\editor\\EditorXssFilter\\InlineStyles' defined in patch #24. style attribute is stripped out from the output.

bgreco’s picture

StatusFileSize
new7.32 KB

Reroll #27 for Drupal 9.1.8. Same security considerations from #9 apply here.

wim leers’s picture

Security risk

While I like the spirit of #2, I'm not sure Drupal core will be willing to take this risk.

Allowing inline styles on user input is a risk, period. Selectively allowing inline styles is less risky, but any bug in the logic that does the selective allowing again implies a security risk.

I'm also not certain that #2 is truly safe. For example, what about using display: none; or color: #fff to make content invisible to a reviewer?

Best practices when theme dictates look and feel

Besides the security implications, using inline styles is generally frowned upon. It makes sense only for sites where the site is willing to grant content authors very far-reaching permissions to deviate from the site's theme. To ensure uniformity, it'd be better to add a class attribute to your tag (e.g. <table class="comparison"> or <table class="reference">) using https://ckeditor.com/cke4/addon/stylescombo (which Drupal core ships with by default). Then the front end developer can specify the styling once.

Landing pages when a site has advanced content authors

That being said, there absolutely are scenarios where you want to fully empower (trusted!) content authors to do anything they want. Typical example: landing pages. In this case, I suggest using the Full HTML text format — or really, any text format without the Limit allowed HTML tags and correct faulty HTML filter (filter plugin ID: filter_html).

Backwards compatibility & visual regressions

Finally, we cannot make the change that is being proposed here without breaking backwards compatibility. If we change filter_html to start allowing certain inline styles to make it into the filtered output, then any site that updates may suddenly suffer from visual regressions.

_dcre_’s picture

After updating core from 8.9.14 to 8.9.16, patch #24 although it applies it does not fix the problem any longer.
Any ideas?

gauravvvv’s picture

StatusFileSize
new7.32 KB
new638 bytes

Re-rolled patch #33. Attached interdiff for same.

Status: Needs review » Needs work

The last submitted patch, 36: 3109650-36.patch, failed testing. View results

loziju’s picture

StatusFileSize
new5.77 KB
new1.76 KB

Re-rolled patch #36 for 9.2.x core.

The same warning applies:
If you have any Wysiwyg editors that are open to non-trusted sources, you should NOT add this patch, as it would introduce a XSS security issue!

junaidpv’s picture

I see the patch from comment #24 no longer works in Drupal 8.9.17

joshua1234511’s picture

StatusFileSize
new7.32 KB

Ignore Patch in incompatible version patch

joshua1234511’s picture

StatusFileSize
new7.68 KB

hook_editor_xss_filter_alter() is not firing anymore, so there is no way to use the '\\Drupal\\editor\\EditorXssFilter\\InlineStyles' defined in patch #24
Updated the Patch to work with latest 8.9

joshua1234511’s picture

StatusFileSize
new7.37 KB

hook_editor_xss_filter_alter() is not firing anymore, so there is no way to use the '\\Drupal\\editor\\EditorXssFilter\\InlineStyles' defined in patch #24
Updated the Patch to work with latest 8.9.x

technoveltyco’s picture

I simplified the code logic of the patch #24. It will make the XSS filter work with style attributes that are not URI types. Regarding to the proposal of overriding the XSS inline filtering, you can still do so by using the code in #4. You can find the new patches for Drupal Core 9.2.x, 9.1.x and 8.9.x.

I already tested the 9.2.x patch in a live project that uses CkEditor and the module CKEditor Table Selection. Below you can see a screenshot with the test results formatting a table in the editor.

Patch #43 test result screenshot

Please try the other versions of the patches and validate if they are working as expected.

Thanks to @adamfranco for the good proposal fixing this issue.

Best regards.

technoveltyco’s picture

Status: Needs work » Needs review
StatusFileSize
new5.5 KB

Sorry but I just realised that the patch created for 8.9.x is not correct. Attaching the right file here:

Status: Needs review » Needs work

Version: 9.1.x-dev » 9.3.x-dev

Drupal 9.1.10 (June 4, 2021) and Drupal 9.2.10 (November 24, 2021) were the last bugfix releases of those minor version series. Drupal 9 bug reports should be targeted for the 9.3.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

playful’s picture

#43 is working for me on D9.2. But is it safe to apply this patch on a live site?

playful’s picture

Actually #43 is not working entirely... some needed styles are still stripped, such as "text-decoration:none"

I'm attempting to use the views Global: Custom text field to generate tables and table rows/cells containing inline styles, but some of the styles are getting stripped on output. The Global: Custom text field doesn't use anything assistive like CKEditor. So why are we not allowed to put whatever we want in our views custom text fields?

Any advice on how I can get my views to output inline styles via a Global: Custom text field?

EDIT: described my workaround for this in issue #3256957

johnpitcairn’s picture

Everybody here seems to be concerned only with text editor filtering, but there is an awful lot of other code that also runs markup through Xss. What I would like is a nice clean way in code to replace Drupal\Component\Utility\Xss with my own extending class and have that used everywhere. If Xss was a service this would be simple.

Due to potential security headaches and comments from core maintainers above, I doubt this issue will ever be anything more than a patch, or a collection of techniques to get around the problem.

Background: I've been messing around with Xss::attributes() to try to allow a specific use-case in a views rendered entity field (ie not sourced from any user-facing input), which is:

<div class="some classes here" style="--varname1: value; --varname2: value">

The style attribute is added by a paragraphs behavior plugin, and the values are validated. This allows passing dynamic values through to css, scoped to the element they are defined on:

div.some.classes.here {
  background-color: var(--varname1);
}

This is a hugely useful technique, but a modification of how Xss::attributes() operates is required.

johnpitcairn’s picture

I've come to a different solution for my views rendered entity use-case above, patching core to provide a simple alter hook directly in Xss::attributes(). Nothing changes unless a module implements the hook. Attributes are not passed by reference so are not alterable within the hook, only the criteria by which core filters them.

In my custom module I implement the hook, checking an explicit data- attribute that my module also sets on the element render array to determine whether to allow style attributes to pass through Xss:attributes unmolested - so this occurs for that element's attributes only:

function MYMODULE_xss_attributes_alter(array &$data) {
  // We don't care unless our data flag AND a style attribute is present.
  if (strpos($data['attributes'], 'data-mymodule-allow-styles) === FALSE
      || strpos($data['attributes'], ' style="') === FALSE) {
    return;
  }
  // Prevent protocol filtering or skipping the style attribute.
  $data['skip_protocol_filtering'][] = 'style';
  foreach ($data['strip_attribute_names'] as $delta => $value) {
    if ($value === 'style') {
      unset($data['strip_attribute_names'][$delta]);
    }
  }
}

I attach a patch that provides this alter hook for anyone wishing to pursue this approach. I don't recommend anyone uses this in production unless they really know what they are doing. In particular, be sure to check the provided attributes are what you expect. The code above could possibly be expanded to check whether the attributes include only styles that my module expects.

lobodakyrylo’s picture

@John Pitcairn I like your solution. Your patch has wrong paths of Xss.php file, I fixed it.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

very_random_man’s picture

I found the patch in #53 useful but i ended up removing it as it's a bit of a sledgehammer solution that affects everything.

I think a better approach would be to simply move the code that defines that config into a separate method rather than calling a hook. You can already use hook_editor_xss_filter_alter to specify your own Xss class and that includes the text format that is currently being used so you can lock it down with more specific conditions.

Then all you need to do is create an Xss filter class that overrides the config and anything you needs and write a hook to enable it for certain format conditions.

ravi.shankar’s picture

StatusFileSize
new2.94 KB
new2.47 KB

Added reroll of the patch #53 on Drupal 9.4.x. and fixed Drupal CS issue of that patch #53.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

ruslan piskarov’s picture

StatusFileSize
new2.97 KB

Added reroll of the patch #56 on Drupal 9.5.x.

ruslan piskarov’s picture

StatusFileSize
new2.94 KB
new2.97 KB

Just renamed #56 and #58. So will not confuse.

_utsavsharma’s picture

StatusFileSize
new483 bytes
new2.98 KB

Tried to fix CCF in #59.
Please review.

johnpitcairn’s picture

@very_random_man re #55: hook_editor_xss_filter_alter() and swapping the class used is only useful if the filtering is happening via a text editor. It's of no help if something else like Views is calling Xss::filter().

eugene.brit’s picture

StatusFileSize
new3.07 KB

The patch from #60 has not applied for D9.5.9
Re-roll for 9.5.9

nadim hossain’s picture

StatusFileSize
new3.07 KB

#60 missing "datetime", #62 was working fine for me in 9.5.9, so rerolled the same patch for 10.1.x

s3b0un3t’s picture

StatusFileSize
new3.67 KB

The proposed patch now only applies to attributes using single quotes. I'm updating the patch (for 9.5.x) to work for double quotes as well.

kwfinken’s picture

Version: 9.5.x-dev » 10.2.x-dev

This should be moved over to D10 as D9 is approaching EOL and D10 is where such maintenance should reside.

krystalcode’s picture

I have come across this recently and I tried to organize the reasoning behind the whole issue and come to a conclusion.

Impact

First, I think I understand the reasoning of the current behavior, but here is the result - which I think is worse than the intentions.

  1. Security-aware developer puts a filter in the text format, such as HTMLPurifier or HtmLawed, in order to find a balance between allowing some inline styles capabilities but mitigating at least some risks.
  2. Drupal core says, sorry developer, you cannot have a partly secure setup. If you need inline styles, go ahead and remove your security filter so that you have a completely insecure setup.
  3. Client goes "I came to Drupal from X system for being a great CMS, but I cannot do such a basic thing like setting the size of some text and adding some custom padding, Drupal is so limiting me".
  4. Account manager sees the dollars going away, ok developer let's remove the security filter, content editors are trusted users.
  5. Attacker gains access to a content editor's account and engineers an XSS attack, or click-jacking, or whatever pleases, through script tags or style attributes.
  6. Administrator user views or edits that content item and is subjected to the attack.

Drupal core allows me as a developer/site builder to have a completely insecure text editing configuration, if that's what I deem appropriate for my use case. But it prevents me from having a reasonably secure setup.

Proposed solutions

First, the idea is that the content is stored "as is" in the database and the filtering happens during rendering. This is intentional because if something would be filtered before saving it into the database, that's gone forever leading to errors. This is exactly what happens though with this issue and it is inconsistent with this design.

What are the disadvantages of delegating the rendering to the text editor? CKEditor has its own system for this, being Advanced Content Filtering in v4 or General HTML Support in v5. Provide the default configuration for what is provided by Drupal core i.e. CKEditor + the default text formats, and let developer configure the text editor as needed if they customize their setup.

Alternatively, would it make sense to load the same security filters that are configured for the frontend rendering - which I believe are determined by the FilterInterface::TYPE_HTML_RESTRICTOR constant, load them in the text editing rendering i.e. in the Standard editor XSS filter?

This way I get the same result in both frontend and text editing rendering. It does violate though the design of not overriding the original data. Could be offered though as an option to the developer to decide.

Keeping the HTML code safe is a massive subject, and that's why there exist specialized libraries for it. Trying to reinvent this within Drupal core (referring to allowing simple properties only within inline styles), will only deliver a mediocre solution, locking developers into it - unless you get into complex efforts to override/extend/replace classes, increase code maintenance for core and contrib/custom modules. Better let developers use the tools that specialize in solving these issues.

I have just reviewed this this week and I am fairly unfamiliar with components involved, so if I'm misunderstanding this part of the stack please do correct me.

Documentation/help

Lastly, after a solution is decided, I think what is really missing is guidance for the developer/site builder. I think that the majority of developers/site builders would not be familiar with all these details, the security risks, what alternative solutions to provide to text editors. So let's create a documentation page with advice like the ones @Wim Leers suggested, plus an explanation of this issue i.e. the frontend/text editing rendering and how to properly configure both the text editor and the text filters, and link to that documentation in the text format create/edit pages.

johnpitcairn’s picture

Re #66: yes, but this issue does not just affect text formats/editors.

It also affects things like views rewrites, where you might want to rewrite and add a style attribute that is benign - passing a css property value through to a stylesheet for example. No text editor is involved there.

bgreco’s picture

Update #43 for 10.1

quentin.le-delas’s picture

StatusFileSize
new3.9 KB

I have update the #64 patch for the last core update 10.2.3

quentin.le-delas’s picture

StatusFileSize
new3.67 KB

update patch with LF format

damienmckenna’s picture

Status: Needs work » Needs review
StatusFileSize
new3.68 KB

This is a reroll of #64/#71 for 10.2.x.

smustgrave’s picture

Version: 10.2.x-dev » 11.x-dev
Status: Needs review » Needs work
Issue tags: +Needs issue summary update, +Needs tests

Recommend using MRs

Feature will need test coverage

Issue summary should use standard template for reviews and committers

paramnida’s picture

Re-rolling #43/68 for 10.2.3

hmdnawaz’s picture

Patch for 10.4.x

vetchneons made their first commit to this issue’s fork.

luongosb’s picture

Patch for 11.0.x

nadeemkmir’s picture

This patch is for 11.1.x

oily’s picture

I am seeing

\Drupal::moduleHandler()->alter('xss_attributes', $attribute_data);

in the latest patches. It seems that this line (among other things) is causing the pipeline to fail. The container is not available at the stage the code is executed at so using \Drupal:: to access ModuleHandler service does not work.

I notice that none of the other utility classes inside the same utility? folder contain the string: '\Drupal::'

oily’s picture

#34 needs to be addressed. This issue cannot progress until those comprehensive security concerns are responded to in equal depth. Has anyone attempted this? Otherwise I think the issue status should be changed to Postponed..

darren oh’s picture

Comment #34 is long out of date. What we are doing now is allowing the XSS tag list to be altered by modules.

oily’s picture

Re: #82, Okay i see. But does that mean that the hooks? should contain like a packet of cigarettes, a 'health warning'? Not sure what the policy on hook security is? Are there hooks that knowingly reduce the level of security achieved by core?

Alternatively, do you mean that by virtue of the fact that an extension point is being exposed so users can choose whether or not to add non-core tags that the security concerns at #34 are irrelevant?

darren oh’s picture

A module could alter the XSS tag list to make it more secure, so the security concern is not relevant to the solution that is being proposed.

arantxio’s picture

Patch for current 10.5.x

kenwest’s picture

It looks like the patch for #85 has collided with Commit 26caa6b1 related to https://www.drupal.org/project/drupal/issues/3511566

The change to line 219 ...

-              in_array($attribute_name, ['style', 'srcdoc']) ||
+              in_array($attribute_name, $attribute_data['strip_attribute_names']) ||

... implies 'strip_attribute_names' should be set to ['style', 'srcdoc'] rather than ['style'].

Until a better patch is rolled, you can a) use the patch at #77 which works on 10.5.4 or b) have the implementation of hook_xss_attributes_alter() set 'strip_attribute_names' correctly

oily’s picture

Issue summary: View changes
oily’s picture

Issue summary: View changes
oily’s picture

jaroslav červený’s picture

I have reworked patch #74 for Drupal core 10.5.6

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

lexbritvin’s picture

TL;DR

Hooks don't work for Xss (core/lib, no DI). This patch adds two granular options: extend the class and override, or pass attribute filters directly to filter(). allowAttribute() included as alternative to hook proposals, but discouraged. No BC break — existing tests should pass.

Detailed

Introducing hooks is not applicable here. Xss is inside core/lib and must be isolated
from the rest of Drupal, DI is not allowed. So I went with configuration on the class
itself.

The refactoring does the same thing as other patches — extracting the skip decision
and value processing out of attributes() into shouldSkipAttribute(),
filterAttributeValue(), and needsProtocolFiltering(). The revision is that
protocol filtering was originally done before checking whether the attribute is
skipped. I moved it into filterAttributeValue(), which only runs if the attribute
makes it through. This way the method actually does something useful to override.

The real benefit here is the more granular approach. It covers class extension and a
new argument to filter().

Class extension: EditorXssFilter\Standard already extends Xss, and
hook_editor_xss_filter_alter already lets you swap it out. What was missing is
anything to actually override — the attribute logic was all inlined. Now you can
provide a new Standard via the hook and reuse the parent, only overriding what you need.

The new argument: a third parameter to filter() for attribute filters — a map of
attribute name to a sanitizer callback, or TRUE to allow with default filtering. If an
attribute is in that map, it won't be skipped. No side effects, you pass what you need
and get filtered output. FilterHtml is the natural fit here. Similar to
Extended HTML Filter, it keeps the scope local to the filter.

I also added Xss::allowAttribute() for global registration on the static class. I
strongly discourage this one — in wrong hands it opens too much. There is obviously
demand for it though, and developers must be able to extend the functionality when
they know what they do. It also allows creating a module that intelligently filters all style attributes. I included it in the patch to give an
alternative to the hook proposals. I don't think it should be accepted in this form
and requires more discussion.

The patch doesn't break existing functionality — existing tests should pass, API evolves but doesn't break.

Extending FilterHtml with the new argument (no core FilterHtml patch needed)

// ./mymodule.module
// Override existing filter_html implementation
function mymodule_filter_info_alter(array &$info) {
  if (isset($info['filter_html'])) {
    $info['filter_html']['class'] = MyFilterHtml::class;
    $info['filter_html']['title'] = \t('[MyModule] Limit allowed HTML tags and correct faulty HTML');
  }
}

// ./src/Plugin/Filter/MyFilterHtml.php
/**
 * Provides an extended filter to limit HTML tags with the ability to use "style" attribute.
 */
class MyFilterHtml extends FilterHtml {

  /**
   * {@inheritdoc}
   */
  public function process($text, $langcode) {
    $restrictions = $this->getHtmlRestrictions();
    // Defines style attribute filter.
    $attribute_filters = [
      'style' => [$this, 'sanitizeStyleAttribute'],
    ];
    unset($restrictions['allowed']['*']);
    $text = Xss::filter($text, array_keys($restrictions['allowed']), $attribute_filters);
    return new FilterProcessResult($this->filterAttributes($text));
  }

  /**
   * {@inheritdoc}
   */
  public function getHTMLRestrictions() {
    $restrictions = $this->restrictions ?? parent::getHTMLRestrictions();
    // Remove global configuration for style attribute.
    // If set to FALSE, it is disallowed globally in HTMLRestrictions.
    // If set to TRUE, it is too much for all and may fail validation.
    /* @see \Drupal\ckeditor5\HTMLRestrictions */
    unset($restrictions['allowed']['*']['style']);
    // Allow style attribute in span elements.
    if (isset($restrictions['allowed']['span'])) {
      $restrictions['allowed']['span']['style'] = TRUE;
    }
    return $restrictions;
  }

  /**
   * Attribute filter for style attribute.
   *
   * @param string $attribute_value
   *   Attribute value to process.
   *
   * @return string
   *   Sanitized attribute value.
   */
  public function sanitizeStyleAttribute($attribute_value) {
    $allowed_style_props = [
      'color',
    ];

    $filtered_props = [];
    foreach (explode(';', $attribute_value) as $prop) {
      // Remove whitespaces
      $prop = trim($prop);
      // Check if the property is in the whitelist.
      if (preg_match('/^([a-z\-]+?):.+?$/', $prop, $matches) && in_array($matches[1], $allowed_style_props, true)) {
        $filtered_props[] = $prop;
      }
    }

    if (!empty($filtered_props)) {
      $filtered_props = implode(';', $filtered_props);
      // Add a semicolon at the end if it does not exist.
      return str_ends_with($filtered_props, ';') ? $filtered_props : $filtered_props . ';';
    }

    return '';
  }

}

Global allowAttribute usage

# mymodule.services.yml
services:
  mymodule.xss_config_subscriber:
    class: Drupal\mymodule\EventSubscriber\XssConfigSubscriber
    tags:
      - { name: event_subscriber }
class XssConfigSubscriber implements EventSubscriberInterface {

  public static function getSubscribedEvents() {
    return [
      KernelEvents::REQUEST => ['onRequest', 2048],
    ];
  }

  public function onRequest() {
    Xss::allowAttribute('style', [CssSanitizer::class, 'sanitize']);
    Xss::allowAttribute('data-custom');
  }
}

Subclassing for the editor XSS filter

function mymodule_editor_xss_filter_alter(&$editor_xss_filter_class, FilterFormatInterface $format, FilterFormatInterface $original_format = NULL) {
  $editor_xss_filter_class = MyXssStandard::class;
}

class MyXssStandard extends Standard {

  /**
   * {@inheritdoc}
   */
  protected static function shouldSkipAttribute($attribute_name, array $attribute_filters) {
    if ($attribute_name === 'style') {
      return FALSE;
    }
    return parent::shouldSkipAttribute($attribute_name, $attribute_filters);
  }

  /**
   * {@inheritdoc}
   */
  protected static function filterAttributeValue($attribute_name, $value, $attribute_filters) {
    if ($attribute_name === 'style') {
      return $value;
    }
    return parent::filterAttributeValue($attribute_name, $value, $attribute_filters);
  }

}