From 0e71ac25b0f18b7561bf32334a711c336fde0e11 Mon Sep 17 00:00:00 2001 From: liaisontw Date: Tue, 28 Apr 2026 12:51:04 +0800 Subject: [PATCH] Ajax: Improve sanitization and screen state in dashboard widget updates. --- src/wp-admin/includes/ajax-actions.php | 6 +- .../tests/ajax/wpAjaxDashboardWidget.php | 154 ++++++++++++++++++ 2 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 tests/phpunit/tests/ajax/wpAjaxDashboardWidget.php diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 2af08fba70af9..8f9c5719fabbc 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -420,12 +420,14 @@ function wp_ajax_get_community_events() { function wp_ajax_dashboard_widgets() { require_once ABSPATH . 'wp-admin/includes/dashboard.php'; - $pagenow = $_GET['pagenow']; + $pagenow = isset( $_GET['pagenow'] ) ? sanitize_key( $_GET['pagenow'] ) : ''; + if ( 'dashboard-user' === $pagenow || 'dashboard-network' === $pagenow || 'dashboard' === $pagenow ) { set_current_screen( $pagenow ); } - switch ( $_GET['widget'] ) { + $widget = isset( $_GET['widget'] ) ? sanitize_key( $_GET['widget'] ) : ''; + switch ( $widget ) { case 'dashboard_primary': wp_dashboard_primary(); break; diff --git a/tests/phpunit/tests/ajax/wpAjaxDashboardWidget.php b/tests/phpunit/tests/ajax/wpAjaxDashboardWidget.php new file mode 100644 index 0000000000000..f39e0d23bdf4f --- /dev/null +++ b/tests/phpunit/tests/ajax/wpAjaxDashboardWidget.php @@ -0,0 +1,154 @@ +user->create( array( 'role' => 'administrator' ) ); + } + + public function set_up(): void { + parent::set_up(); + + wp_set_current_user( self::$admin_id ); + set_current_screen( 'dashboard' ); + + //Prevent time waste due to all external HTTP requests from RSS feeds + add_filter( 'pre_http_request', '__return_true' ); + $GLOBALS['post'] = null; + } + + public function tear_down(): void { + set_current_screen( 'front' ); + unset( $_GET['pagenow'], $_GET['widget'], $_POST['post_ID'], $_POST['action'] ); + unset( $GLOBALS['post'] ); + parent::tear_down(); + } + + /** + * wp_ajax_dashboard_widgets Happy Path。 + * + * All valid inputs should correctly set the current screen and output the expected widget content. + */ + public function test_wp_ajax_dashboard_widgets_happy_path() { + $this->_setRole( 'administrator' ); + + $_GET['pagenow'] = 'dashboard'; + $_GET['widget'] = 'dashboard_primary'; + $_POST['action'] = 'dashboard-widgets'; + + update_option( 'dashboard_primary_feeds', array( 'test' => array( 'link' => 'https://wordpress.org' ) ) ); + + try { + $this->_handleAjax( 'dashboard-widgets' ); + } catch ( \WPAjaxDieContinueException $e ) { // 捕捉所有 AJAX 死亡例外 (包括 Stop 和 Continue) + unset( $e ); + } + + $output = $this->_last_response; + + $this->assertStringContainsString( 'rss-widget', $output ); + $this->assertSame( 'dashboard', $GLOBALS['current_screen']->id ); + } + + /** + * Test empty parameters should not trigger PHP Warning + * + * @ticket 65054 + */ + public function test_wp_ajax_dashboard_widgets_empty_params() { + $this->_setRole( 'administrator' ); + $_GET = array(); + $_POST['action'] = 'dashboard-widgets'; + + try { + $this->_handleAjax( 'dashboard-widgets' ); + } catch ( \Exception $e ) { + $this->assertTrue( true ); + } + + $this->assertEmpty( $this->_last_response ); + } + + /** + * Test that the current screen is not affected by global post + * + * @ticket 65054 + */ + public function test_wp_ajax_dashboard_widgets_should_not_be_affected_by_global_post() { + //Should Not See This + $pollution_post_id = self::factory()->post->create( array( 'post_title' => 'Should Not See This' ) ); + $GLOBALS['post'] = get_post( $pollution_post_id ); + + $_GET['pagenow'] = 'dashboard'; + $_GET['widget'] = 'dashboard_primary'; + $_GET['action'] = 'dashboard-widgets'; + + wp_dashboard_setup(); + try { + $this->_handleAjax( 'dashboard-widgets' ); + } catch ( \WPAjaxDieContinueException $e ) { + unset( $e ); + } + + $output = $this->_last_response; + + $this->assertStringContainsString( 'rss-widget', $output ); + $this->assertSame( 'dashboard', $GLOBALS['current_screen']->id ); + } + + /** + * Invalid request should not trigger global fallback + * + * @ticket 65054 + */ + public function test_wp_ajax_dashboard_widgets_invalid_request_no_fallback() { + $_GET['widget'] = 'non_existent_widget'; + $_POST['action'] = 'dashboard-widgets'; + + try { + $this->_handleAjax( 'dashboard-widgets' ); + } catch ( \Exception $e ) { + $is_ajax_die = $e instanceof \WPAjaxDieStopException || $e instanceof \WPAjaxDieContinueException; + $this->assertTrue( $is_ajax_die, 'Captured exception should be an AJAX die exception' ); + } + + $this->assertEmpty( $this->_last_response ); + } + + /** + * Test that when an invalid pagenow is passed, the screen should not be changed. + * + * @ticket 65054 + */ + public function test_wp_ajax_dashboard_widgets_invalid_pagenow() { + $this->_setRole( 'administrator' ); + + $_GET['pagenow'] = 'malicious-script-tag'; //malicious-script-tag + $_GET['widget'] = 'dashboard_primary'; + + try { + $this->_handleAjax( 'dashboard-widgets' ); + } catch ( \WPAjaxDieContinueException $e ) { + unset( $e ); + } + + $this->assertNotSame( 'malicious-script-tag', $GLOBALS['current_screen']->id ); + } +}