Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
7c01020
Relax style tag contents validation
sirreal Dec 17, 2025
511284e
Apply suggestions from code review
sirreal Dec 17, 2025
edca2a2
Update comments and document in since annotation
sirreal Dec 17, 2025
db273b7
Update customizer error message
sirreal Dec 17, 2025
62d59c2
Update wp_custom_css_cb to rely on HTML API for safe SCRIPT tag print…
sirreal Dec 17, 2025
f3a1716
Wrap customizer CSS test in newlines
sirreal Dec 17, 2025
e793caa
Use HTML API for style tags in script-loader
sirreal Dec 17, 2025
aa7852c
Use HTML Tag Processor to produce WP_Styles style tags
sirreal Dec 17, 2025
93cddc3
Use HTML Tag Processor for STYLE tags in theme.php
sirreal Dec 17, 2025
41a1fe3
PICKME: Update styles tests to use assertEqualHTML
sirreal Dec 17, 2025
99b8733
Simplify conditional style no-print test
sirreal Dec 17, 2025
2ea6a12
Correctly check presence with stripos
sirreal Dec 18, 2025
81679af
Merge branch 'trunk' into 64418/improve-custom-css-handling
sirreal Dec 19, 2025
e3a6d78
Merge branch 'trunk' into 64418/improve-custom-css-handling
sirreal Dec 22, 2025
2c19861
Update failing test, add test for allowed CSS
sirreal Dec 22, 2025
05a45e1
Merge branch 'trunk' into 64418/improve-custom-css-handling
sirreal Dec 23, 2025
c124eb5
Update wp_custom_css_cb to rely on HTML API for safe SCRIPT tag print…
sirreal Dec 17, 2025
e055156
Wrap customizer CSS test in newlines
sirreal Dec 17, 2025
33f9616
Use HTML API for style tags in script-loader
sirreal Dec 17, 2025
606539e
Use HTML Tag Processor to produce WP_Styles style tags
sirreal Dec 17, 2025
c938d4c
Use HTML Tag Processor for STYLE tags in theme.php
sirreal Dec 17, 2025
dd919f1
Build font style tags with HTML API
sirreal Dec 23, 2025
d29900a
PICKME: Update font tests to use semantic HTML comparison
sirreal Dec 23, 2025
6c6a72b
Use HTML API for hide header text
sirreal Dec 23, 2025
aad4744
Revert "Use HTML API for hide header text"
sirreal Dec 23, 2025
96849ff
Disable KSES content filter, prevent mangling CSS-in-JSON
sirreal Dec 26, 2025
1ab58ec
Revert "Disable KSES content filter, prevent mangling CSS-in-JSON"
sirreal Dec 26, 2025
ac2c8e6
Escape "<" and ">" in JSON to protect tag-like syntax
sirreal Dec 26, 2025
c3ae9a9
Merge branch 'trunk' into styles/use-html-api-for-style-tags
sirreal Dec 26, 2025
4e88745
Fix lint
sirreal Dec 26, 2025
c8f8a4d
Add test with bare & to trick KSES more
sirreal Dec 26, 2025
731bce7
Escape "&" in JSON to protect & and things that appear to be HTML cha…
sirreal Dec 26, 2025
1eb76e7
Merge branch 'trunk' into 64418/improve-custom-css-handling
sirreal Dec 29, 2025
99b1ba4
Fix lint
sirreal Dec 26, 2025
d296d6c
Merge branch 'trunk' into styles/use-html-api-for-style-tags
sirreal Dec 29, 2025
0050789
Revert HTML API STYLE tag generation
sirreal Dec 29, 2025
d3ac6a8
Merge branch 'trunk' into 64418/improve-custom-css-handling
sirreal Dec 29, 2025
b185a8c
Improve invalid CSS message.
sirreal Dec 29, 2025
96ea3c4
Remove arbitrary CSS content validation
sirreal Dec 29, 2025
56e19b5
Revert customizer custom_css changes
sirreal Dec 29, 2025
d9ae831
Encode data in the prepare_for_database method
sirreal Dec 29, 2025
12a6775
Merge branch 'styles/use-html-api-for-style-tags' into 64418/improve-…
sirreal Dec 29, 2025
0141653
Restore STYLE tag trailing newline
sirreal Dec 29, 2025
6585099
Restore STYLE tag trailing newlines in theme.php
sirreal Dec 29, 2025
5a8ce12
Merge branch 'styles/use-html-api-for-style-tags' into 64418/improve-…
sirreal Dec 29, 2025
30ca7cb
Cross-reference escaping in REST controller
sirreal Dec 29, 2025
e1322b0
Deprecate validate_custom_css method
sirreal Dec 30, 2025
407d43f
Move trailing newline out of Tag Processor
sirreal Dec 30, 2025
ccf1625
Merge branch 'styles/use-html-api-for-style-tags' into 64418/improve-…
sirreal Dec 30, 2025
adb0a13
Merge branch 'trunk' into 64418/improve-custom-css-handling
sirreal Dec 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/wp-includes/kses.php
Original file line number Diff line number Diff line change
Expand Up @@ -2474,7 +2474,13 @@ function wp_filter_global_styles_post( $data ) {
$data_to_encode = WP_Theme_JSON::remove_insecure_properties( $decoded_data, 'custom' );

$data_to_encode['isGlobalStylesUserThemeJSON'] = true;
return wp_slash( wp_json_encode( $data_to_encode ) );
/**
* JSON encode the data stored in post content.
* Escape characters that are likely be mangled by HTML filters: "<>&".
*
* 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 ) );
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

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'm not deeply familiar with global styles, so I was happy to get @ramonjd's review (and more are welcome).

It's hard to imagine using this data without parsing the JSON. Compliant JSON parsers will parse this correctly. Anything that relies on this data being JSON encoded in a specific way (with specific escaping) would be surprising and does not seem like behavior that needs to be supported.

That said, I know that the global styles system will parse the JSON:

$decoded_data = json_decode( $user_cpt['post_content'], true );

As will the REST endpoint:

$raw_config = json_decode( $post->post_content, true );

The REST API later responds with a JSON body, but that encoding happens elsewhere and the client is expected to parse the JSON response.

Copy link
Member

@ramonjd ramonjd Dec 30, 2025

Choose a reason for hiding this comment

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

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?

In my limited knowledge, and as far as I'm aware it's expected that the content will eventually meet json_decode(), which handles things identically.

Should there be a test case to confirm this?

if my understanding is correct, this is attempting to JSON-parse every single post of every single kind of post type on save

Oh I see it's running through _save_pre hooks, so I understand the same. Bit before my time, but I like your optimization idea to check for { and } before parsing.

My intellectual crutch on these matters is usually @oandregal and @andrewserong

That said, I know that the global styles system will parse the JSON:
As will the REST endpoint:

This is the state as I understand it as well:

  • json_decode in WP_Theme_JSON_Resolver to parse and merge user data for the most recent published post for internal processing, rendering CSS etc
  • json_decode in WP_REST_Global_Styles_Controller to parse handle a specific wp_global_styles post by ID in REST

Cheers!

}
return $data;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,6 @@ protected function prepare_item_for_database( $request ) {
if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) {
$config = array();
if ( isset( $request['styles'] ) ) {
if ( isset( $request['styles']['css'] ) ) {
$css_validation_result = $this->validate_custom_css( $request['styles']['css'] );
if ( is_wp_error( $css_validation_result ) ) {
return $css_validation_result;
}
}
$config['styles'] = $request['styles'];
} elseif ( isset( $existing_config['styles'] ) ) {
$config['styles'] = $existing_config['styles'];
Expand All @@ -275,7 +269,14 @@ protected function prepare_item_for_database( $request ) {
}
$config['isGlobalStylesUserThemeJSON'] = true;
$config['version'] = WP_Theme_JSON::LATEST_SCHEMA;
$changes->post_content = wp_json_encode( $config );
/**
* JSON encode the data stored in post content.
* 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.
Copy link
Member

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

Exactly.

*/
$changes->post_content = wp_json_encode( $config, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP );
}

// Post title.
Expand Down Expand Up @@ -657,24 +658,17 @@ public function get_theme_items( $request ) {
}

/**
* Validate style.css as valid CSS.
*
* Currently just checks for invalid markup.
*
* @since 6.2.0
* @since 6.4.0 Changed method visibility to protected.
* @deprecated 7.0.0 This method is deprecated and always returns true.
*
* @ignore
*
* @param string $css CSS to validate.
* @return true|WP_Error True if the input was validated, otherwise WP_Error.
* @return true|WP_Error Always returns true.
*/
protected function validate_custom_css( $css ) {
Copy link
Member

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?

Copy link
Member

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_Controller in 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;
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Good feedback. In e1322b0 I've:

  • removed the call to the method
  • Added a _deprecated_function() to the method.
  • Added relevant annotations to the methods documentation to mark it as deprecated and to exclude it from generated documentation.
  • I removed the method's description since the method should not be used or documented.

if ( preg_match( '#</?\w+#', $css ) ) {
return new WP_Error(
'rest_custom_css_illegal_markup',
__( 'Markup is not allowed in CSS.' ),
array( 'status' => 400 )
);
}
_deprecated_function( __METHOD__, '7.0.0' );
return true;
}
}
53 changes: 34 additions & 19 deletions tests/phpunit/tests/rest-api/rest-global-styles-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -647,25 +647,6 @@ public function test_update_item_valid_styles_css() {
$this->assertSame( 'body { color: red; }', $data['styles']['css'] );
}

/**
* @covers WP_REST_Global_Styles_Controller::update_item
* @ticket 57536
*/
public function test_update_item_invalid_styles_css() {
wp_set_current_user( self::$admin_id );
if ( is_multisite() ) {
grant_super_admin( self::$admin_id );
}
$request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id );
$request->set_body_params(
array(
'styles' => array( 'css' => '<p>test</p> body { color: red; }' ),
)
);
$response = rest_get_server()->dispatch( $request );
$this->assertErrorResponse( 'rest_custom_css_illegal_markup', $response, 400 );
}

/**
* Tests the submission of a custom block style variation that was defined
* within a theme style variation and wouldn't be registered at the time
Expand Down Expand Up @@ -826,4 +807,38 @@ public function test_global_styles_route_args_schema() {
$this->assertArrayHasKey( 'type', $route_data[0]['args']['id'] );
$this->assertSame( 'integer', $route_data[0]['args']['id']['type'] );
}

/**
* @covers WP_REST_Global_Styles_Controller::update_item
* @ticket 64418
*/
public function test_update_allows_valid_css_with_more_syntax() {
wp_set_current_user( self::$admin_id );
if ( is_multisite() ) {
grant_super_admin( self::$admin_id );
}
$request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id );
$css = <<<'CSS'
@property --animate {
syntax: "<custom-ident>";
inherits: true;
initial-value: false;
}
h1::before { content: "fun & games"; }
CSS;
$request->set_body_params(
array(
'styles' => array( 'css' => $css ),
)
);

$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();
$this->assertSame( $css, $data['styles']['css'] );

// Compare expected API output to WP internal values.
$request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id );
$response = rest_get_server()->dispatch( $request );
$this->assertSame( $css, $response->get_data()['styles']['css'] );
}
}
Loading