Skip to content

Conversation

@sirreal
Copy link
Member

@sirreal sirreal commented Dec 17, 2025

  • Allow arbitrary text in global styles custom CSS.
  • Protect the CSS data from mangling by KSES HTML filters.

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 flags JSON_HEX_TAG and JSON_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.

@github-actions
Copy link

Test using WordPress Playground

The 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

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@sirreal sirreal force-pushed the 64418/improve-custom-css-handling branch from c0dab9b to 99b8733 Compare December 17, 2025 12:54
@sirreal
Copy link
Member Author

sirreal commented Dec 18, 2025

As I explored the space, this PR grew in scope. There are at least two concerns that I plan to split up:

  • Use the HTML API to correctly produce STYLE tags across the codebase.
  • Relax CSS "validation," (requires additional safety on STYLE tags).

@sirreal
Copy link
Member Author

sirreal commented Dec 22, 2025

YES! KSES indeed breaks this as can be seen in the multisite tests:

1) WP_REST_Global_Styles_Controller_Test::test_update_allows_valid_css_with_more_syntax
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
 '@property --animate {
-	syntax: "<custom-ident>";
+	syntax: "";
 	inherits: true;
 	initial-value: false;
 }'

That's almost certainly a missing unfiltered_html cap triggering KSES and strip_html.

@sirreal
Copy link
Member Author

sirreal commented Dec 30, 2025

An aside, this will have to be synced with Gutenberg:

https://github.com/WordPress/gutenberg/blob/trunk/lib/class-wp-rest-global-styles-controller-gutenberg.php

Thank you for flagging that @ramonjd. This is an interesting case and I'd like to get your feedback.

This depends on the work in #10656. That's another Core change to how STYLE tags are constructed. Without that change, unsafe styles can be produced. That's a problem if Gutenberg removes the CSS content restrictions because it won't include the necessary changes to STYLE tags.

How should we approach that?

One option is to maintain the validation in such a way that unsafe CSS is not allowed, @dmsnell shared a suitable snippet in this comment. This relaxes the check and should be safe to include in Gutenberg today because the STYLE tag cannot be prematurely closed.

@ramonjd
Copy link
Member

ramonjd commented Dec 30, 2025

This depends on the work in #10656. That's another Core change to how STYLE tags are constructed. Without that change, unsafe styles can be produced. That's a problem if Gutenberg removes the CSS content restrictions because it won't include the necessary changes to STYLE tags.

Dang! Can I take that back about syncing? :trollface:

Yeah, I think we'll have to have validate_custom_css do something if plugin users aren't running 7.0 or whatever version these changes will live in.

In my opinion, Dennis's comment is a safe bet.

perhaps we should deprecate it and leave in the check.

Or, since it's plugin-only, use Dennis's new and improved version! Does that sound reasonable?

@sirreal
Copy link
Member Author

sirreal commented Dec 30, 2025

I'm quite ignorant on a lot of this.

The last change to that file in Gutenberg was WordPress/gutenberg@f77a466 (Sep 18, 2024).

  • Does Gutenberg need to keep its version or can it be removed and rely on Core now?
  • Does it strictly need to be in sync ASAP, or could it come into sync when Core is ready?
  • Gutenberg could use a simplified version of the check (a stripos() check for </style is probably sufficient) until it requires WordPress 7.0, then fully sync and remove the validation check.

@ramonjd
Copy link
Member

ramonjd commented Dec 31, 2025

Good questions:

Does Gutenberg need to keep its version or can it be removed and rely on Core now?

For context, the global styles controller was created as a dedicated, bundled class because the Core equivalent was being extended and overridden so frequently, and referenced others classes like WP_Theme_JSON and others that were also being extended and overridden, that it became a burden to deal with the compat files and classes.

For example, the controller relies on WP_Theme_JSON_Resolver_Gutenberg or WP_Theme_JSON_Gutenberg, which has been modified in the plugin, and then these might further rely on modified functions/methods and so on.

My instinct is that Gutenberg needs to keep its changes for this reason.

Folks are pretty good at migrating said changes to Core, and theoretically they should be synced in that direction (plugin > Core).

Does it strictly need to be in sync ASAP, or could it come into sync when Core is ready?

I reckon after this merges is fine. Not speaking specifically to this PR, but if Core introduces a bugfix/new feature in global styles controller is generally good to sync that back before the next plugin update so plugin users get the benefit (since the plugin uses it's own class WP_REST_Global_Styles_Controller_Gutenberg)

Gutenberg could use a simplified version of the check (a stripos() check for </style is probably sufficient) until it requires WordPress 7.0, then fully sync and remove the validation check.

This sounds wholly reasonable to me. I appreciate you being on top of this! Thank you!

@jorgefilipecosta jorgefilipecosta self-requested a review December 31, 2025 09:29
@sirreal
Copy link
Member Author

sirreal commented Jan 2, 2026

@dmsnell and I discussed this recently. We decided to:

  • restore the validate_custom_css check
  • implement it as a check for </style in the CSS

This is likely sufficient to allow the valid CSS folks want to use without introducing problems in Gutenberg for WordPress versions that won't include the improved safety when constructing style tags from #10656.

@sirreal
Copy link
Member Author

sirreal commented Jan 5, 2026

I've updated the check to disallow:

  • CSS content that will close a STYLE tag (</style + tag name terminator: " \t\r\f\n/>").
  • CSS content that ends with a partial style tag closer (<, </, … </style)

These changes should make the style tag contents safe to save, print, and concatenate with other arbitrary contents even without the STYLE tag change in #10656. They are safe to port to Gutenberg.

(This was another approach I discussed with @dmsnell and it seems like the most appropriate.)

Copy link
Member

@ramonjd ramonjd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is working well for me. Do folks have any other test cases or concerns?

I've prepped a Gutenberg PR over at

I think this could go in at any time.

sprintf(
/* translators: %s is the CSS that was provided. */
__( 'The CSS must not end in "%s".' ),
esc_html( substr( $css, $at ) )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanted to confirm that when I try to save unsafe CSS content (with a closing style tag) I see:

"The CSS must not contain \"&lt;\/style&gt;\"."

👍🏻

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need good wording here. it doesn’t require the closing >. what @sirreal mentioned is more accurate, but I think not the most helpful either. it requires a case-insensitive match for <style followed by a tag name terminator, which could be a > or a whitespace character or a solidus /.

The CSS must not prematurely end the STYLE element: e.g. </style>.

I’ve found it really hard to be terse and clear, but I anticipate the response, “I don’t have </style>, I have </style and swag/>.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's difficult to print a generic error message that's informative and doesn't look like a typo to the average user (</style seems like it's missing a character).

That's one reason I decided to use the more specific checks. It simplifies printing an informative and accurate error message:

The CSS must not end in "%s".

(This could be <, </,… </styl, etc.)

or

The CSS must not contain "%s".

(This could be </style>, </style , </style/, etc.)

Where %s is the problematic text (HTML escaped).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are unit tests that include the error messages.

The client doesn't do anything helpful with the error messages:

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request enhances the Global Styles custom CSS handling to allow arbitrary CSS content while protecting it from corruption by WordPress KSES HTML filtering. The solution uses JSON encoding flags (JSON_HEX_TAG and JSON_HEX_AMP) to escape characters that could be mangled when KSES processes the post content as HTML.

Key changes:

  • Improved CSS validation to allow legitimate CSS containing < and & characters while blocking actual </style> closing tags
  • Added JSON encoding flags to escape potentially problematic characters in both REST API and KSES filter functions
  • Comprehensive test coverage for validation logic including edge cases

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php Replaced strict regex validation with precise algorithm that allows CSS with HTML-like syntax while blocking style tag closure; added JSON_HEX_TAG and JSON_HEX_AMP flags to JSON encoding
src/wp-includes/kses.php Added same JSON encoding flags to wp_filter_global_styles_post() for consistency with REST API
tests/phpunit/tests/rest-api/rest-global-styles-controller.php Added comprehensive test coverage for new validation logic including allowed CSS patterns and various forms of disallowed style tag closures

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

sirreal and others added 2 commits January 8, 2026 14:11
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants