From dedac14f183cfb7365977d334a70e2bddea82109 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/2] 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 d58f689456c7947ce6b0cf2671b035359099c188 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/2] 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 );