Skip to content

Commit 889afa9

Browse files
lucadobrescuSoare-Robert-Daniel
authored andcommitted
fix: strip tags from chart custom CSS at render time (#1355)
1 parent 7b30054 commit 889afa9

3 files changed

Lines changed: 94 additions & 1 deletion

File tree

classes/Visualizer/Module.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ protected function get_inline_custom_css( $id, $settings ) {
650650
$class_name = $id . $name;
651651
$properties = implode( ' !important; ', array_filter( $attributes ) );
652652
if ( ! empty( $properties ) ) {
653-
$css .= '.' . $class_name . ' {' . $properties . ' !important;}';
653+
$css .= wp_strip_all_tags( '.' . $class_name . ' {' . $properties . ' !important;}' );
654654
$classes[ $name ] = $class_name;
655655
}
656656
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
/**
3+
* E2E test helper (loaded as an mu-plugin via .wp-env.json).
4+
*
5+
* Lets specs plant chart data/settings meta directly, e.g. a stored-XSS
6+
* payload in `customcss` that UI save flows strip on submission, so
7+
* render-time sanitization can be verified.
8+
*/
9+
add_action(
10+
'rest_api_init',
11+
function () {
12+
register_rest_route(
13+
'visualizer-e2e/v1',
14+
'/chart-settings/(?P<id>\d+)',
15+
array(
16+
'methods' => 'POST',
17+
'permission_callback' => function () {
18+
return current_user_can( 'manage_options' );
19+
},
20+
'callback' => function ( WP_REST_Request $request ) {
21+
$chart_id = (int) $request['id'];
22+
$body = $request->get_json_params();
23+
if ( isset( $body['settings'] ) ) {
24+
update_post_meta( $chart_id, 'visualizer-settings', $body['settings'] );
25+
}
26+
if ( isset( $body['series'] ) ) {
27+
update_post_meta( $chart_id, 'visualizer-series', $body['series'] );
28+
}
29+
if ( isset( $body['content'] ) ) {
30+
wp_update_post(
31+
array(
32+
'ID' => $chart_id,
33+
'post_content' => maybe_serialize( $body['content'] ),
34+
)
35+
);
36+
}
37+
return array( 'ok' => true );
38+
},
39+
)
40+
);
41+
}
42+
);

tests/e2e/specs/custom-css.spec.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
5+
6+
let chartId;
7+
8+
test.describe( 'Custom CSS sanitization', () => {
9+
test.beforeAll( async ( { requestUtils } ) => {
10+
const chart = await requestUtils.rest( {
11+
method: 'POST',
12+
path: '/wp/v2/visualizer',
13+
data: { title: 'Custom CSS payload chart', status: 'publish' },
14+
} );
15+
chartId = chart.id;
16+
17+
await requestUtils.rest( {
18+
method: 'POST',
19+
path: `/visualizer-e2e/v1/chart-settings/${ chartId }`,
20+
data: {
21+
settings: {
22+
customcss: {
23+
title: {
24+
color: 'red</style><script>window.vizXss=1</script><style>',
25+
'font-size': '12px',
26+
},
27+
},
28+
},
29+
},
30+
} );
31+
} );
32+
33+
test.afterAll( async ( { requestUtils } ) => {
34+
if ( chartId ) {
35+
await requestUtils.rest( { method: 'DELETE', path: `/wp/v2/visualizer/${ chartId }`, params: { force: true } } );
36+
}
37+
} );
38+
39+
test( 'strips tags from chart custom CSS on the library page', async ( { admin, page } ) => {
40+
await admin.visitAdminPage( 'admin.php?page=visualizer' );
41+
42+
const styleBlock = page.locator( `#customcss-visualizer-${ chartId }` );
43+
await expect( styleBlock ).toHaveCount( 1 );
44+
// Legitimate rules survive sanitization. <style> has no innerText, so read textContent.
45+
const css = await styleBlock.textContent();
46+
expect( css ).toContain( 'font-size: 12px' );
47+
expect( css ).not.toContain( '<script' );
48+
// The injected script must not have executed.
49+
expect( await page.evaluate( () => window.vizXss ) ).toBeUndefined();
50+
} );
51+
} );

0 commit comments

Comments
 (0)