From baa0294e08664973ad1d7b24e2f7b406bebd98fe Mon Sep 17 00:00:00 2001 From: Tiago Hillebrandt Date: Thu, 23 Oct 2025 03:19:42 -0400 Subject: [PATCH 1/4] Adds capability check for AJAX requests. --- src/Admin/AJAX.php | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/Admin/AJAX.php b/src/Admin/AJAX.php index 7fe0064..b29836b 100644 --- a/src/Admin/AJAX.php +++ b/src/Admin/AJAX.php @@ -88,7 +88,7 @@ public function is_doing_ajax() { */ public function hide_admin_notice() { - check_ajax_referer( 'supv_hide_admin_notice' ); + $this->verify_ajax_request( 'hide_admin_notice' ); if ( ! empty( $_POST['software'] ) && preg_match( '/(?:ssl|https)/', sanitize_key( wp_unslash( $_POST['software'] ) ) ) ) { $notices_transient = get_transient( Dashboard::HIDE_NOTICES_TRANSIENT ); @@ -112,7 +112,7 @@ public function hide_admin_notice() { */ public function transients_cleanup() { - check_ajax_referer( 'supv_transients_cleanup' ); + $this->verify_ajax_request( 'transients_cleanup' ); supv()->core()->transients()->cleanup( isset( $_POST['expired'] ) ); @@ -156,7 +156,7 @@ public function autoload_options_history() { */ public function autoload_update_option() { - check_ajax_referer( 'supv_autoload_update_option' ); + $this->verify_ajax_request( 'autoload_update_option' ); $data = $this->extract_form_data(); @@ -184,7 +184,7 @@ public function autoload_update_option() { */ public function wordpress_auto_update_policy() { - check_ajax_referer( 'supv_wordpress_auto_update_policy' ); + $this->verify_ajax_request( 'wordpress_auto_update_policy' ); $policy = ! empty( $_POST['wp_auto_update_policy'] ) ? sanitize_key( wp_unslash( $_POST['wp_auto_update_policy'] ) ) : false; @@ -204,7 +204,7 @@ public function wordpress_auto_update_policy() { */ public function secure_login_settings_output() { - check_ajax_referer( 'supv_secure_login_settings_output' ); + $this->verify_ajax_request( 'secure_login_settings_output' ); supv()->core()->secure_login()->update_settings( [ @@ -224,7 +224,7 @@ public function secure_login_settings_output() { */ public function secure_login_settings_save() { - check_ajax_referer( 'supv_secure_login_settings_save' ); + $this->verify_ajax_request( 'secure_login_settings_save' ); $settings = array_map( 'intval', $this->extract_form_data() ); // Converts all the values to int. @@ -267,4 +267,20 @@ private function extract_form_data() { // phpcs:enable WordPress.Security.NonceVerification.Missing } + + /** + * Verifies AJAX request security (nonce and capability). + * + * @since {VERSION} + * + * @param string $action The action name for nonce verification. + */ + private function verify_ajax_request( $action ) { + + check_ajax_referer( 'supv_' . $action ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( __( 'You do not have permission to perform this action.', 'supervisor' ) ); + } + } } From d04e56e8a0e33b78f8362b85376b6935fc98233d Mon Sep 17 00:00:00 2001 From: Tiago Hillebrandt Date: Thu, 23 Oct 2025 03:28:46 -0400 Subject: [PATCH 2/4] Verifies AJAX requests for autoload options. Improves the method to extract form data. --- src/Admin/AJAX.php | 26 ++++++++++++++------------ src/Core/WordPress.php | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Admin/AJAX.php b/src/Admin/AJAX.php index b29836b..3f189db 100644 --- a/src/Admin/AJAX.php +++ b/src/Admin/AJAX.php @@ -128,7 +128,7 @@ public function transients_cleanup() { */ public function autoload_options_list() { - check_ajax_referer( 'supv_autoload_options_list' ); + $this->verify_ajax_request( 'autoload_options_list' ); ( new AutoloadCardView() )->output_options(); @@ -142,7 +142,7 @@ public function autoload_options_list() { */ public function autoload_options_history() { - check_ajax_referer( 'supv_autoload_options_history' ); + $this->verify_ajax_request( 'autoload_options_history' ); ( new AutoloadCardView() )->output_history(); @@ -244,28 +244,30 @@ public function secure_login_settings_save() { */ private function extract_form_data() { - // phpcs:disable WordPress.Security.NonceVerification.Missing + $data = []; + $prefix = 'supv-field-'; - $data = []; + foreach ( $_POST as $key => $value ) { + // Decode and sanitize the field name. + $sanitized_key = sanitize_key( urldecode( $key ) ); - foreach ( array_keys( $_POST ) as $key ) { - if ( ! preg_match( '/^supv-field-/', sanitize_key( $key ) ) ) { + // Skip if not a supervisor field. + if ( strpos( $sanitized_key, $prefix ) !== 0 ) { continue; } - $value = ! empty( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : ''; - $field = preg_replace( '/^supv-field-/', '', urldecode( sanitize_key( $key ) ) ); + // Extract field name by removing prefix. + $field_name = substr( $sanitized_key, strlen( $prefix ) ); - if ( empty( $field ) ) { + if ( empty( $field_name ) ) { continue; } - $data[ $field ] = $value; + // Sanitize and store the value. + $data[ $field_name ] = sanitize_text_field( wp_unslash( $value ) ); } return $data; - - // phpcs:enable WordPress.Security.NonceVerification.Missing } /** diff --git a/src/Core/WordPress.php b/src/Core/WordPress.php index 1f3545a..1de252d 100644 --- a/src/Core/WordPress.php +++ b/src/Core/WordPress.php @@ -70,7 +70,7 @@ public function apply_wp_auto_update_policy() { // phpcs:ignore WPForms.PHP.Hook */ public function set_auto_update_policy( $policy ) { - if ( $this->get_auto_update_policy() ) { + if ( $this->get_auto_update_policy() !== false ) { update_option( self::CORE_AUTO_UPDATE_OPTION, $policy ); } } From b45ff79276dcfe55b4551d63dc36866225832d63 Mon Sep 17 00:00:00 2001 From: Tiago Hillebrandt Date: Thu, 23 Oct 2025 03:51:27 -0400 Subject: [PATCH 3/4] Fixes nonce notification. --- src/Admin/AJAX.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Admin/AJAX.php b/src/Admin/AJAX.php index 3f189db..c5e9850 100644 --- a/src/Admin/AJAX.php +++ b/src/Admin/AJAX.php @@ -247,6 +247,8 @@ private function extract_form_data() { $data = []; $prefix = 'supv-field-'; + // phpcs:disable WordPress.Security.NonceVerification.Missing + foreach ( $_POST as $key => $value ) { // Decode and sanitize the field name. $sanitized_key = sanitize_key( urldecode( $key ) ); @@ -267,6 +269,8 @@ private function extract_form_data() { $data[ $field_name ] = sanitize_text_field( wp_unslash( $value ) ); } + // phpcs:enable WordPress.Security.NonceVerification.Missing + return $data; } From 583222e265101c0b14d27b5da59beed18d0e6382 Mon Sep 17 00:00:00 2001 From: Tiago Hillebrandt Date: Thu, 23 Oct 2025 04:30:18 -0400 Subject: [PATCH 4/4] Implements the Request helper class. --- inc/helpers.php | 7 +++- src/Admin/AJAX.php | 19 +++++----- src/Helpers/Request.php | 81 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 src/Helpers/Request.php diff --git a/inc/helpers.php b/inc/helpers.php index 1c1340a..d248b13 100644 --- a/inc/helpers.php +++ b/inc/helpers.php @@ -1,4 +1,7 @@ verify_ajax_request( 'hide_admin_notice' ); - if ( ! empty( $_POST['software'] ) && preg_match( '/(?:ssl|https)/', sanitize_key( wp_unslash( $_POST['software'] ) ) ) ) { + $software = RequestHelper::get_post_arg( 'software', null, 'sanitize_key' ); + + if ( ! empty( $software ) && preg_match( '/(?:ssl|https)/', $software ) ) { $notices_transient = get_transient( Dashboard::HIDE_NOTICES_TRANSIENT ); if ( $notices_transient === false ) { $notices_transient = []; } - $notices_transient[ sanitize_key( wp_unslash( $_POST['software'] ) ) ] = 1; + $notices_transient[ $software ] = 1; set_transient( Dashboard::HIDE_NOTICES_TRANSIENT, $notices_transient, DAY_IN_SECONDS ); } @@ -114,7 +117,9 @@ public function transients_cleanup() { $this->verify_ajax_request( 'transients_cleanup' ); - supv()->core()->transients()->cleanup( isset( $_POST['expired'] ) ); + $expired = RequestHelper::get_post_arg( 'expired', false ); + + supv()->core()->transients()->cleanup( (bool) $expired ); ( new TransientsCardView() )->output_stats( true ); @@ -186,7 +191,7 @@ public function wordpress_auto_update_policy() { $this->verify_ajax_request( 'wordpress_auto_update_policy' ); - $policy = ! empty( $_POST['wp_auto_update_policy'] ) ? sanitize_key( wp_unslash( $_POST['wp_auto_update_policy'] ) ) : false; + $policy = RequestHelper::get_post_arg( 'wp_auto_update_policy', null, 'sanitize_key' ); if ( ! empty( $policy ) && preg_match( '/^(?:minor|major|disabled|dev)$/', $policy ) ) { supv()->core()->wordpress()->set_auto_update_policy( $policy ); @@ -247,9 +252,7 @@ private function extract_form_data() { $data = []; $prefix = 'supv-field-'; - // phpcs:disable WordPress.Security.NonceVerification.Missing - - foreach ( $_POST as $key => $value ) { + foreach ( $_POST as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing // Decode and sanitize the field name. $sanitized_key = sanitize_key( urldecode( $key ) ); @@ -269,8 +272,6 @@ private function extract_form_data() { $data[ $field_name ] = sanitize_text_field( wp_unslash( $value ) ); } - // phpcs:enable WordPress.Security.NonceVerification.Missing - return $data; } diff --git a/src/Helpers/Request.php b/src/Helpers/Request.php new file mode 100644 index 0000000..301b5c3 --- /dev/null +++ b/src/Helpers/Request.php @@ -0,0 +1,81 @@ +