From ff2548632dca15c89d434e879fdfc1c37af3f38c Mon Sep 17 00:00:00 2001 From: lucadobrescu <252785083+lucadobrescu@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:36:48 +0300 Subject: [PATCH 1/3] fix: gate setup wizard endpoints behind manage_options and nonces Co-authored-by: Cursor --- classes/Visualizer/Module/Wizard.php | 9 ++++++ templates/setup-wizard.php | 13 +++++--- tests/e2e/specs/wizard-auth.spec.js | 46 ++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 tests/e2e/specs/wizard-auth.spec.js diff --git a/classes/Visualizer/Module/Wizard.php b/classes/Visualizer/Module/Wizard.php index 3a1007521..c3256e61d 100644 --- a/classes/Visualizer/Module/Wizard.php +++ b/classes/Visualizer/Module/Wizard.php @@ -153,6 +153,12 @@ public function visualizer_enqueue_setup_wizard_scripts() { * @return bool|void */ public function dismissWizard( $redirect_to_dashboard = true ) { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have permission to perform this action.', 'visualizer' ), '', array( 'response' => 403 ) ); + } + if ( false !== $redirect_to_dashboard ) { + check_admin_referer( 'visualizer_dismiss_wizard' ); + } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $status = isset( $_REQUEST['status'] ) ? (int) $_REQUEST['status'] : 0; update_option( 'visualizer_fresh_install', $status ); @@ -169,6 +175,9 @@ public function dismissWizard( $redirect_to_dashboard = true ) { */ public function visualizer_wizard_step_process() { check_ajax_referer( VISUALIZER_ABSPATH, 'security' ); + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json( array( 'status' => 0 ), 403 ); + } $step = ! empty( $_POST['step'] ) ? sanitize_text_field( wp_unslash( $_POST['step'] ) ) : 1; switch ( $step ) { case 'step_2': diff --git a/templates/setup-wizard.php b/templates/setup-wizard.php index e78178027..eccd04163 100644 --- a/templates/setup-wizard.php +++ b/templates/setup-wizard.php @@ -6,12 +6,15 @@ * @package Templates */ -$dashboard_url = add_query_arg( - array( - 'action' => 'visualizer_dismiss_wizard', - 'status' => 0, +$dashboard_url = wp_nonce_url( + add_query_arg( + array( + 'action' => 'visualizer_dismiss_wizard', + 'status' => 0, + ), + admin_url( 'admin.php' ) ), - admin_url( 'admin.php' ) + 'visualizer_dismiss_wizard' ); $chart_id = ! empty( $this->wizard_data['chart_id'] ) ? (int) $this->wizard_data['chart_id'] : ''; diff --git a/tests/e2e/specs/wizard-auth.spec.js b/tests/e2e/specs/wizard-auth.spec.js new file mode 100644 index 000000000..4ca3599f2 --- /dev/null +++ b/tests/e2e/specs/wizard-auth.spec.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +const CONTRIBUTOR = { username: 'viz_wiz_contributor', password: 'viz-wiz-contributor-pass', email: 'viz-wiz-contributor@example.com' }; + +let contributorId; + +test.describe( 'Setup wizard authorization', () => { + test.beforeAll( async ( { requestUtils } ) => { + const contributor = await requestUtils.rest( { + method: 'POST', + path: '/wp/v2/users', + data: { ...CONTRIBUTOR, roles: [ 'contributor' ] }, + } ); + contributorId = contributor.id; + } ); + + test.afterAll( async ( { requestUtils } ) => { + if ( contributorId ) { + await requestUtils.rest( { method: 'DELETE', path: `/wp/v2/users/${ contributorId }`, params: { force: true, reassign: 1 } } ); + } + } ); + + test( 'denies wizard dismissal for non-admin users', async ( { browser } ) => { + const context = await browser.newContext( { baseURL: test.info().project.use.baseURL } ); + const page = await context.newPage(); + await page.goto( '/wp-login.php' ); + await page.fill( '#user_login', CONTRIBUTOR.username ); + await page.fill( '#user_pass', CONTRIBUTOR.password ); + await page.click( '#wp-submit' ); + await page.waitForURL( '**/wp-admin/**' ); + + const response = await page.goto( '/wp-admin/admin.php?action=visualizer_dismiss_wizard&status=1' ); + expect( response.status() ).toBe( 403 ); + + await context.close(); + } ); + + test( 'requires a nonce for wizard dismissal', async ( { page } ) => { + // Logged in as admin (default storage state) but without a nonce. + const response = await page.goto( '/wp-admin/admin.php?action=visualizer_dismiss_wizard&status=1' ); + expect( response.status() ).toBe( 403 ); + } ); +} ); From c7e6b35a567b6bec6d67d4f6ae82920fd7d131aa Mon Sep 17 00:00:00 2001 From: lucadobrescu <252785083+lucadobrescu@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:53:01 +0300 Subject: [PATCH 2/3] test: log in via wp-login POST instead of dashboard navigation Co-authored-by: Cursor --- tests/e2e/specs/wizard-auth.spec.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/e2e/specs/wizard-auth.spec.js b/tests/e2e/specs/wizard-auth.spec.js index 4ca3599f2..d232675a8 100644 --- a/tests/e2e/specs/wizard-auth.spec.js +++ b/tests/e2e/specs/wizard-auth.spec.js @@ -25,13 +25,20 @@ test.describe( 'Setup wizard authorization', () => { test( 'denies wizard dismissal for non-admin users', async ( { browser } ) => { const context = await browser.newContext( { baseURL: test.info().project.use.baseURL } ); - const page = await context.newPage(); - await page.goto( '/wp-login.php' ); - await page.fill( '#user_login', CONTRIBUTOR.username ); - await page.fill( '#user_pass', CONTRIBUTOR.password ); - await page.click( '#wp-submit' ); - await page.waitForURL( '**/wp-admin/**' ); + // POST wp-login with redirects off: the auth cookie is set by the 302 + // response, so we never load the wp-admin dashboard (can stall in CI). + const loginResponse = await context.request.post( '/wp-login.php', { + form: { + log: CONTRIBUTOR.username, + pwd: CONTRIBUTOR.password, + 'wp-submit': 'Log In', + testcookie: '1', + }, + maxRedirects: 0, + } ); + expect( loginResponse.status() ).toBe( 302 ); + const page = await context.newPage(); const response = await page.goto( '/wp-admin/admin.php?action=visualizer_dismiss_wizard&status=1' ); expect( response.status() ).toBe( 403 ); From d63f945670b66366c0a3890fdc3b7c7b17756816 Mon Sep 17 00:00:00 2001 From: lucadobrescu Date: Thu, 30 Jul 2026 11:15:25 +0300 Subject: [PATCH 3/3] fix: strip tags from chart custom CSS at render time (#1355) --- classes/Visualizer/Module.php | 2 +- .../mu-plugins/plant-chart-settings.php | 42 +++++++++++++++ tests/e2e/specs/custom-css.spec.js | 51 +++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/config/mu-plugins/plant-chart-settings.php create mode 100644 tests/e2e/specs/custom-css.spec.js diff --git a/classes/Visualizer/Module.php b/classes/Visualizer/Module.php index 13d7cbfc1..ae7993917 100644 --- a/classes/Visualizer/Module.php +++ b/classes/Visualizer/Module.php @@ -650,7 +650,7 @@ protected function get_inline_custom_css( $id, $settings ) { $class_name = $id . $name; $properties = implode( ' !important; ', array_filter( $attributes ) ); if ( ! empty( $properties ) ) { - $css .= '.' . $class_name . ' {' . $properties . ' !important;}'; + $css .= wp_strip_all_tags( '.' . $class_name . ' {' . $properties . ' !important;}' ); $classes[ $name ] = $class_name; } } diff --git a/tests/e2e/config/mu-plugins/plant-chart-settings.php b/tests/e2e/config/mu-plugins/plant-chart-settings.php new file mode 100644 index 000000000..5222e9de8 --- /dev/null +++ b/tests/e2e/config/mu-plugins/plant-chart-settings.php @@ -0,0 +1,42 @@ +\d+)', + array( + 'methods' => 'POST', + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + 'callback' => function ( WP_REST_Request $request ) { + $chart_id = (int) $request['id']; + $body = $request->get_json_params(); + if ( isset( $body['settings'] ) ) { + update_post_meta( $chart_id, 'visualizer-settings', $body['settings'] ); + } + if ( isset( $body['series'] ) ) { + update_post_meta( $chart_id, 'visualizer-series', $body['series'] ); + } + if ( isset( $body['content'] ) ) { + wp_update_post( + array( + 'ID' => $chart_id, + 'post_content' => maybe_serialize( $body['content'] ), + ) + ); + } + return array( 'ok' => true ); + }, + ) + ); + } +); diff --git a/tests/e2e/specs/custom-css.spec.js b/tests/e2e/specs/custom-css.spec.js new file mode 100644 index 000000000..1b2fb22f5 --- /dev/null +++ b/tests/e2e/specs/custom-css.spec.js @@ -0,0 +1,51 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +let chartId; + +test.describe( 'Custom CSS sanitization', () => { + test.beforeAll( async ( { requestUtils } ) => { + const chart = await requestUtils.rest( { + method: 'POST', + path: '/wp/v2/visualizer', + data: { title: 'Custom CSS payload chart', status: 'publish' }, + } ); + chartId = chart.id; + + await requestUtils.rest( { + method: 'POST', + path: `/visualizer-e2e/v1/chart-settings/${ chartId }`, + data: { + settings: { + customcss: { + title: { + color: 'red