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..d232675a8 --- /dev/null +++ b/tests/e2e/specs/wizard-auth.spec.js @@ -0,0 +1,53 @@ +/** + * 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 } ); + // 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 ); + + 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 ); + } ); +} );