Make WordPress Core


Ignore:
Timestamp:
01/26/2026 03:17:00 PM (5 months ago)
Author:
jonsurrell
Message:

Customize: Allow arbitrary custom CSS.

Update custom CSS validation to allow any CSS except STYLE close tags. Previously, some valid CSS would be rejected for containing HTML syntax characters, like this example:

@property --animate {
  syntax: "<custom-ident>"; /* <-- Validation error on `<` */
  inherits: true;
  initial-value: false;
}

Developed in https://github.com/WordPress/wordpress-develop/pull/10667.

Follow-up to [61418], [61486].

Props jonsurrell, westonruter, peterwilsoncc, johnbillion, xknown, sabernhardt, dmsnell, soyebsalar01, dlh.
Fixes #64418.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/customize/custom-css-setting.php

    r60253 r61527  
    376376
    377377    /**
     378     * Ensure that dangerous STYLE tag contents do not break HTML output.
     379     *
     380     * @ticket 64418
     381     * @covers ::wp_update_custom_css_post
     382     * @covers ::wp_custom_css_cb
     383     */
     384    public function test_wp_custom_css_cb_escapes_dangerous_html() {
     385        wp_update_custom_css_post(
     386            '*::before { content: "</style><script>alert(1)</script>"; }',
     387            array(
     388                'stylesheet' => $this->setting->stylesheet,
     389            )
     390        );
     391        $output   = get_echo( 'wp_custom_css_cb' );
     392        $expected =
     393            <<<'HTML'
     394            <style id="wp-custom-css">
     395            *::before { content: "\3c\2fstyle><script>alert(1)</script>"; }
     396            </style>
     397
     398            HTML;
     399        $this->assertEqualHTML( $expected, $output );
     400    }
     401
     402    /**
    378403     * Tests that validation errors are caught appropriately.
    379404     *
     
    383408     * @covers WP_Customize_Custom_CSS_Setting::validate
    384409     */
    385     public function test_validate() {
    386 
     410    public function test_validate_basic_css() {
    387411        // Empty CSS throws no errors.
    388412        $result = $this->setting->validate( '' );
     
    394418        $this->assertTrue( $result );
    395419
    396         // Check for markup.
     420        // Check for illegal closing STYLE tag.
    397421        $unclosed_comment = $basic_css . '</style>';
    398422        $result           = $this->setting->validate( $unclosed_comment );
    399423        $this->assertArrayHasKey( 'illegal_markup', $result->errors );
    400424    }
     425
     426    /**
     427     * @ticket 64418
     428     * @covers WP_Customize_Custom_CSS_Setting::validate
     429     */
     430    public function test_validate_accepts_css_property_at_rule() {
     431        $css =
     432            <<<'CSS'
     433            @property --animate {
     434                syntax: "<custom-ident>";
     435                inherits: true;
     436                initial-value: false;
     437            }
     438            CSS;
     439        $this->assertTrue( $this->setting->validate( $css ) );
     440    }
     441
     442    /**
     443     * @ticket 64418
     444     * @covers ::wp_update_custom_css_post
     445     * @covers ::wp_custom_css_cb
     446     */
     447    public function test_save_and_print_property_at_rule() {
     448        $css =
     449            <<<'CSS'
     450            @property --animate {
     451                syntax: "<custom-ident>";
     452                inherits: true;
     453                initial-value: false;
     454            }
     455            CSS;
     456        wp_update_custom_css_post( $css, array( 'stylesheet' => $this->setting->stylesheet ) );
     457        $output   = get_echo( 'wp_custom_css_cb' );
     458        $expected = "<style id='wp-custom-css'>\n{$css}\n</style>\n";
     459        $this->assertEqualHTML( $expected, $output );
     460    }
     461
     462    /**
     463     * @dataProvider data_custom_css_disallowed
     464     *
     465     * @ticket 64418
     466     * @covers WP_Customize_Custom_CSS_Setting::validate
     467     */
     468    public function test_validate_prevents( $css, $expected_error_message ) {
     469        $result = $this->setting->validate( $css );
     470        $this->assertWPError( $result );
     471        $this->assertSame( $expected_error_message, $result->get_error_message() );
     472    }
     473
     474    /**
     475     * Data provider.
     476     *
     477     * @return array<string, string[]>
     478     */
     479    public static function data_custom_css_disallowed(): array {
     480        return array(
     481            'style close tag'            => array( 'css…</style>…css', 'The CSS must not contain "&lt;/style&gt;".' ),
     482            'style close tag upper case' => array( '</STYLE>', 'The CSS must not contain "&lt;/STYLE&gt;".' ),
     483            'style close tag mixed case' => array( '</sTyLe>', 'The CSS must not contain "&lt;/sTyLe&gt;".' ),
     484            'style close tag in comment' => array( '/*</style>*/', 'The CSS must not contain "&lt;/style&gt;".' ),
     485            'style close tag (/)'        => array( '</style/', 'The CSS must not contain "&lt;/style/".' ),
     486            'style close tag (\t)'       => array( "</style\t", "The CSS must not contain \"&lt;/style\t\"." ),
     487            'style close tag (\f)'       => array( "</style\f", "The CSS must not contain \"&lt;/style\f\"." ),
     488            'style close tag (\r)'       => array( "</style\r", "The CSS must not contain \"&lt;/style\r\"." ),
     489            'style close tag (\n)'       => array( "</style\n", "The CSS must not contain \"&lt;/style\n\"." ),
     490            'style close tag (" ")'      => array( '</style ', 'The CSS must not contain "&lt;/style ".' ),
     491            'truncated "<"'              => array( '<', 'The CSS must not end in "&lt;".' ),
     492            'truncated "</"'             => array( '</', 'The CSS must not end in "&lt;/".' ),
     493            'truncated "</s"'            => array( '</s', 'The CSS must not end in "&lt;/s".' ),
     494            'truncated "</ST"'           => array( '</ST', 'The CSS must not end in "&lt;/ST".' ),
     495            'truncated "</sty"'          => array( '</sty', 'The CSS must not end in "&lt;/sty".' ),
     496            'truncated "</STYL"'         => array( '</STYL', 'The CSS must not end in "&lt;/STYL".' ),
     497            'truncated "</stYle"'        => array( '</stYle', 'The CSS must not end in "&lt;/stYle".' ),
     498        );
     499    }
    401500}
Note: See TracChangeset for help on using the changeset viewer.