-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Global Styles: Allow arbitrary CSS, protect from KSES mangling #10641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Global Styles: Allow arbitrary CSS, protect from KSES mangling #10641
Conversation
STYLE tags may contain any raw text up to a closing STYLE tag https://html.spec.whatwg.org/multipage/parsing.html#generic-raw-text-element-parsing-algorithm
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
src/wp-includes/customize/class-wp-customize-custom-css-setting.php
Outdated
Show resolved
Hide resolved
src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php
Outdated
Show resolved
Hide resolved
Strictly speaking, a `STYLE` tag will be closed by a `STYLE` close tag. However, the CSS contents are concatenated together and the strict check for a STYLE close tag may later be invalidated by concatenation.
c0dab9b to
99b8733
Compare
| $processor->next_tag(); | ||
| $processor->set_attribute( 'id', "{$handle}-inline-css" ); | ||
| $processor->set_modifiable_text( "\n{$inline_style}\n" ); | ||
| $inline_style_tag = $processor->get_updated_html(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it might be worth calling next_token() here to verify that there are no further tokens, as we expect none. if we get one, it means that something inside of $this->type_attr broke out of the tag and potentially out of the style.
in fact, if we do this before setting the modifiable text we can ensure those attributes don’t mess with the STYLE contents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can also leverage wp_kses_hair() (when fixed to rely on the HTML API) and transfer the attributes, but this is tantamount to my suggestion above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is malformed type_attr is something we need to worry about. I did a quick search to see if it is being abused, and I don't see it. If someone were to do something bad with the attribute, then this would be _doing_it_wrong. I've actually been thinking lately that we might want to eliminate the $type_attr entirely as it is obsolete now with HTML5.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks. I didn’t look into it any further. this appears different in nature as well from some of our other HTML5 theme-support checks: we’re not changing the markup structure, so maybe we can remove it.
there is a case where this could lead to styling change, and that’s when someone is targeting the STYLE element based on the presence of the type attribute, such as style[type~=css], which I think would match or fail to match based on the presence of this attribute.
so maybe we propose dropping it entirely and always omit the type_attr since there is no place where having it with the privately-set value of text/css would lead to different interpretations than not having it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is a case where this could lead to styling change, and that’s when someone is targeting the
STYLEelement based on the presence of thetypeattribute, such asstyle[type~=css], which I think would match or fail to match based on the presence of this attribute.
How meta 😄 I'm not aware of style rules having selectors which target other STYLE tags, but anything is possible. This seems extremely unlikely, however.
so maybe we propose dropping it entirely and always omit the
type_attrsince there is no place where having it with the privately-set value oftext/csswould lead to different interpretations than not having it.
I would endorse removing the type attribute since stylesheets are CSS by default, though this wouldn't have to be done as part of this PR. The same could be done for SCRIPT tags being JavaScript as well, but that's out of scope here. The $type_attr is a vestige of the XHTML/HTML4 past.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not aware of style rules having selectors which target other STYLE tags
I guess I said styling change but was also thinking more about document.querySelector(). Either way, I think we are in agreement.
Good point about the <script> type as well, and on doing it in another change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tasked in Core-64428
src/wp-includes/customize/class-wp-customize-custom-css-setting.php
Outdated
Show resolved
Hide resolved
src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php
Outdated
Show resolved
Hide resolved
|
As I explored the space, this PR grew in scope. There are at least two concerns that I plan to split up:
|
Co-authored-by: Weston Ruter <westonruter@gmail.com>
|
YES! KSES indeed breaks this as can be seen in the multisite tests: That's almost certainly a missing |
2ae4e3e to
1eb76e7
Compare
This moves to WordPress#10656
The HTML API will make the content safe to print. The validation serves no purpose.
…custom-css-handling
Co-authored-by: Weston Ruter <westonruter@gmail.com>
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
| * Escape characters that are likely be mangled by HTML filters: "<>&". | ||
| * | ||
| * This data is later re-encoded by {@see wp_filter_global_styles_post}. | ||
| * The escaping is also applied here as a precaution. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thus bypassing the need to do any KSES dance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly.
Co-authored-by: Weston Ruter <westonruter@gmail.com>
…custom-css-handling
| $processor->next_tag(); | ||
| $processor->set_attribute( 'id', "{$handle}-inline-css" ); | ||
| $processor->set_modifiable_text( "\n{$output}\n" ); | ||
| echo $processor->get_updated_html(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a bit of personal preference, but @westonruter do you have thoughts about adding the newline on the echo instead of in the Tag Processor? it seems a bit odd to me seeing it there when our purpose is building the tag.
I support the changes as they are, but I do find echo "{$processor->get_updated_html()}\n"; a bit nicer than adding an extra whitespace-only text node into the HTML parsing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping that newline token outside of the tag processor is cool to me. So yeah, printing the newline after like you have looks good. If that could made consistent in each case, especially.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm coming at this pretty fresh, but from a global styles perspective, it's looking good to me.
I tested on top of #1065
Not sure if this helps you, but I smoke tested the scenario in question (@property --animate), as well as:
- nested CSS, including
@media@keyframes@supports@layer - weird attribute selectors
- CSS vars
- CSS functions like
clamp()calc()etc - HTML tags - I'm seeing the HTML API's escaping in action
\3c\2fstyle>and no injected javascript executes
I can save "Additional CSS" without error and the frontend rendering is as expected.
An aside, this will have to be synced with Gutenberg:
I'm happy to do it if you don't have time once this PR lands.
Thanks a lot for working on this! 🙇🏻
| * @param string $css CSS to validate. | ||
| * @return true|WP_Error True if the input was validated, otherwise WP_Error. | ||
| */ | ||
| protected function validate_custom_css( $css ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think this method needs deprecation now that it returns true?
Also, would it be worth documenting why arbitrary CSS is allowed, how it's protected, and what users should know?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a long rant here and something happened and the Github page reloaded and it’s gone, but I came to say similar things. I don’t want to repeat it all, but the salient points can be distilled:
- there are zero matches for classes extending
WP_REST_Global_Styles_Controllerin the Directory, and the only call to this method in Core is on line 257 above. We can remove that check if we’re going to defang it here. - proprietary or private code might still call this through a subclass. perhaps we should deprecate it and leave in the check.
- the DocBlock comment was almost meaningless and now is actively misleading. at least we could have said something like ”detects if the given CSS might prematurely close its containing STYLE element” but “validate as valid” is not only ambiguous here, but entirely inaccurate, as is the new change to “not break” HTML. the function now does absolutely nothing.
if we leave this in then I’m more in favor of deprecating it, removing the call above, and potentially even expanding the check to be more precise.
/**
* Returns an error if the given CSS would prematurely end
* a containing STYLE element, or `true` otherwise.
*/
protected function validate_custom_css( $css ) {
$at = 0;
$end = strlen( $css ) - 7; // Minimum length of terminating end tag prefix.
while ( $at < $end ) {
$at = stripos( $css, '</style', $at );
if ( false === $at ) {
return true;
}
$at += 7;
if ( 1 === strspn( $css, " \t\f\r\n/>", $at, 1 ) ) {
return new WP_Error( … );
}
}
return true;
}| * | ||
| * This matches the escaping in {@see WP_REST_Global_Styles_Controller::prepare_item_for_database}. | ||
| */ | ||
| return wp_slash( wp_json_encode( $data_to_encode, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well this is a surprising function, and one perhaps that warrants considerably more refactoring. if my understanding is correct, this is attempting to JSON-parse every single post of every single kind of post type on save, even though its name seems to suggest that it’s designed to process global styles posts.
maybe @jorgefilipecosta has some context that is’t obvious.
at a minimum, it surprises me we aren’t even checking if the first and last bytes are { and }, since those are the only allowable JSON. on the positive side, I guess json_decode() will fail quickly, but only after cloning the string and performing string replacements on it via wp_unslash().
on to the question I wanted to discuss on this line, which is a question about backwards compatibility: will this change any existing code? or should it preserve properly given the fact that this should run directly into json_decode() on the other end and parse to the same values?
I would want us to be careful about anything else in the pipeline which might start interpreting content differently given the escaping. my guess is that this is fine, but I’d like to have some confirmation on some of the differences in encoding and how those will be interpreted later on and in the editor and on render.
Under some circumstances KSES would run post content filters and change the resulting content like this:
@property --animate { - syntax: "<custom-ident>"; + syntax: ""; inherits: true; initial-value: false; }The Custom CSS is stored as JSON-encoded data in post content. KSES filters this content as HTML.
I explored a change that would remove and restore the offending KSES filters when saving the custom CSS posts, however the simplest way of protecting the data is by escaping the JSON-encoded data so that it does not contain HTML-like syntax.
<>&and Unicode-escaped by the JSON flagsJSON_HEX_TAGandJSON_HEX_AMP.This PR does not change the Customizer Custom CSS handling. That will be done separately.
This depends on #10656 to ensure styles output is safely printed in the HTML (merged here).
Trac ticket: https://core.trac.wordpress.org/ticket/64418
To do:
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.