PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
← All changes | admin/class-metasync-dev-panel.php +61 -21 2.6.7 → 2.7.1 View file →
@@ -40,8 +40,9 @@
40 40 /**
41 41 * Option name for storing the developer panel password.
42 42 */
43 43 const PASSWORD_OPTION = 'metasync_dev_panel_password';
44 + const PASSWORD_HASH_PREFIX = 'metasync-hash:';
44 45
45 46 /**
46 47 * Authentication manager instance.
47 48 *
@@ -72,8 +73,47 @@
72 73 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
73 74 }
74 75
75 76 /**
77 + * Hash and store the developer tools password.
78 + *
79 + * @param string $password Plaintext password.
80 + * @return bool Whether the option was updated.
81 + */
82 + private function store_hashed_password( $password ) {
83 + $stored = self::PASSWORD_HASH_PREFIX . wp_hash_password( $password );
84 +
85 + return update_option( self::PASSWORD_OPTION, $stored );
86 + }
87 +
88 + /**
89 + * Verify the stored password and migrate legacy plaintext values on success.
90 + *
91 + * @param string $submitted Submitted plaintext password.
92 + * @return bool Whether the password is valid.
93 + */
94 + private function verify_dev_password( $submitted ) {
95 + $stored = get_option( self::PASSWORD_OPTION, '' );
96 +
97 + if ( ! is_string( $stored ) || $stored === '' ) {
98 + return false;
99 + }
100 +
101 + if ( strpos( $stored, self::PASSWORD_HASH_PREFIX ) === 0 ) {
102 + $hash = substr( $stored, strlen( self::PASSWORD_HASH_PREFIX ) );
103 +
104 + return wp_check_password( $submitted, $hash );
105 + }
106 +
107 + if ( hash_equals( $stored, $submitted ) ) {
108 + $this->store_hashed_password( $submitted );
109 + return true;
110 + }
111 +
112 + return false;
113 + }
114 +
115 + /**
76 116 * Register the developer panel menu (hidden from menu, accessible via URL).
77 117 */
78 118 public function add_dev_panel_menu() {
79 119 // Add hidden submenu page (no parent = hidden from menu)
@@ -102,9 +142,9 @@
102 142 return;
103 143 }
104 144
105 145 // Don't show on dev panel page itself
106 - $current_page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : '';
146 + $current_page = isset( $_GET['page'] ) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check
107 147 if ( $current_page === Metasync_Admin::$page_slug . '-dev-tools' ) {
108 148 return;
109 149 }
110 150
@@ -129,9 +169,9 @@
129 169 * @param string $hook The current admin page.
130 170 */
131 171 public function enqueue_scripts( $hook ) {
132 172 // Only load on dev panel page
133 - if ( isset( $_GET['page'] ) && $_GET['page'] === Metasync_Admin::$page_slug . '-dev-tools' ) {
173 + if ( isset( $_GET['page'] ) && $_GET['page'] === Metasync_Admin::$page_slug . '-dev-tools' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check
134 174 wp_enqueue_style( 'metasync-admin' );
135 175 }
136 176 }
137 177
@@ -171,10 +211,10 @@
171 211
172 212 // Handle password setup submission
173 213 if ( isset( $_POST['dev_panel_setup_submit'] ) ) {
174 214 if ( wp_verify_nonce( $_POST['dev_panel_setup_nonce'], 'metasync_dev_panel_setup' ) ) {
175 - $new_password = sanitize_text_field( $_POST['dev_panel_new_password'] );
176 - $confirm_password = sanitize_text_field( $_POST['dev_panel_confirm_password'] );
215 + $new_password = isset( $_POST['dev_panel_new_password'] ) ? (string) wp_unslash( $_POST['dev_panel_new_password'] ) : '';
216 + $confirm_password = isset( $_POST['dev_panel_confirm_password'] ) ? (string) wp_unslash( $_POST['dev_panel_confirm_password'] ) : '';
177 217
178 218 if ( empty( $new_password ) ) {
179 219 $setup_error = 'Password cannot be empty.';
180 220 } elseif ( $new_password !== $confirm_password ) {
@@ -181,9 +221,9 @@
181 221 $setup_error = 'Passwords do not match.';
182 222 } elseif ( strlen( $new_password ) < 6 ) {
183 223 $setup_error = 'Password must be at least 6 characters long.';
184 224 } else {
185 - $result = update_option( self::PASSWORD_OPTION, $new_password );
225 + $result = $this->store_hashed_password( $new_password );
186 226 if ( $result ) {
187 227 // Grant access immediately
188 228 if ( $this->auth ) {
189 229 $this->auth->grant_transient_access();
@@ -277,12 +317,12 @@
277 317
278 318 // Handle password submission
279 319 if ( isset( $_POST['dev_panel_password_submit'] ) ) {
280 320 if ( wp_verify_nonce( $_POST['dev_panel_nonce'], 'metasync_dev_panel_nonce' ) ) {
281 - $submitted_password = sanitize_text_field( $_POST['dev_panel_password'] );
282 - $saved_password = get_option( self::PASSWORD_OPTION, '' );
321 + $submitted_password = isset( $_POST['dev_panel_password'] ) ? (string) wp_unslash( $_POST['dev_panel_password'] ) : '';
283 322
284 - if ( $this->auth && $this->auth->verify_and_grant( $submitted_password, $saved_password, false ) ) {
323 + if ( $this->auth && $this->verify_dev_password( $submitted_password ) ) {
324 + $this->auth->grant_transient_access();
285 325 // Refresh page to show authenticated state - add timestamp to prevent caching
286 326 wp_safe_redirect( add_query_arg( 'login', 'success', $_SERVER['REQUEST_URI'] ) );
287 327 exit;
288 328 } else {
@@ -358,9 +398,9 @@
358 398
359 399 // Handle mode switch
360 400 if ( isset( $_POST['switch_endpoints_submit'] ) ) {
361 401 if ( wp_verify_nonce( $_POST['switch_endpoints_nonce'], 'metasync_switch_endpoints' ) ) {
362 - $new_mode = sanitize_text_field( $_POST['endpoint_mode'] );
402 + $new_mode = sanitize_text_field(wp_unslash($_POST['endpoint_mode']));
363 403 $result = Metasync_Endpoint_Manager::set_mode( $new_mode );
364 404
365 405 if ( $result ) {
366 406 $message = 'Successfully switched to ' . esc_html( $new_mode ) . ' mode!';
@@ -376,9 +416,9 @@
376 416
377 417 // Handle password update
378 418 if ( isset( $_POST['update_password_submit'] ) ) {
379 419 if ( wp_verify_nonce( $_POST['update_password_nonce'], 'metasync_update_dev_password' ) ) {
380 - $new_password = sanitize_text_field( $_POST['dev_panel_new_password'] );
420 + $new_password = isset( $_POST['dev_panel_new_password'] ) ? (string) wp_unslash( $_POST['dev_panel_new_password'] ) : '';
381 421
382 422 if ( empty( $new_password ) ) {
383 423 $message = 'Password cannot be empty.';
384 424 $message_type = 'error';
@@ -385,9 +425,9 @@
385 425 } elseif ( strlen( $new_password ) < 6 ) {
386 426 $message = 'Password must be at least 6 characters long.';
387 427 $message_type = 'error';
388 428 } else {
389 - $result = update_option( self::PASSWORD_OPTION, $new_password );
429 + $result = $this->store_hashed_password( $new_password );
390 430 if ( $result ) {
391 431 $message = 'Password updated successfully!';
392 432 $message_type = 'success';
393 433 } else {
@@ -467,9 +507,9 @@
467 507 </label>
468 508 </div>
469 509
470 510 <button type="submit" name="switch_endpoints_submit" value="1" class="button button-primary">
471 - <span class="dashicons dashicons-controls-repeat" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Switch Endpoints
511 + <span class="dashicons dashicons-controls-repeat"></span> Switch Endpoints
472 512 </button>
473 513 </form>
474 514 </div>
475 515
@@ -533,11 +573,11 @@
533 573 public function ajax_switch_endpoints() {
534 574 // Verify nonce
535 575 check_ajax_referer( 'metasync_switch_endpoints', 'nonce' );
536 576
537 - // Verify permissions (matches main plugin's access control)
538 - if ( ! Metasync::current_user_has_plugin_access() ) {
539 - wp_send_json_error( array( 'message' => 'Insufficient permissions' ) );
577 + // Developer tools are administrator-only.
578 + if ( ! current_user_can( 'manage_options' ) ) {
579 + wp_send_json_error( array( 'message' => 'Insufficient permissions' ), 403 );
540 580 return;
541 581 }
542 582
543 583 // Verify authentication
@@ -546,9 +586,9 @@
546 586 return;
547 587 }
548 588
549 589 // Get and validate mode
550 - $mode = isset( $_POST['endpoint_mode'] ) ? sanitize_text_field( $_POST['endpoint_mode'] ) : '';
590 + $mode = isset( $_POST['endpoint_mode'] ) ? sanitize_text_field(wp_unslash($_POST['endpoint_mode'])) : '';
551 591 if ( ! in_array( $mode, array( 'production', 'staging' ), true ) ) {
552 592 wp_send_json_error( array( 'message' => 'Invalid mode' ) );
553 593 return;
554 594 }
@@ -575,11 +615,11 @@
575 615 public function ajax_update_dev_password() {
576 616 // Verify nonce
577 617 check_ajax_referer( 'metasync_update_dev_password', 'nonce' );
578 618
579 - // Verify permissions (matches main plugin's access control)
580 - if ( ! Metasync::current_user_has_plugin_access() ) {
581 - wp_send_json_error( array( 'message' => 'Insufficient permissions' ) );
619 + // Developer tools are administrator-only.
620 + if ( ! current_user_can( 'manage_options' ) ) {
621 + wp_send_json_error( array( 'message' => 'Insufficient permissions' ), 403 );
582 622 return;
583 623 }
584 624
585 625 // Verify authentication
@@ -588,9 +628,9 @@
588 628 return;
589 629 }
590 630
591 631 // Get new password
592 - $new_password = isset( $_POST['dev_panel_password'] ) ? sanitize_text_field( $_POST['dev_panel_password'] ) : '';
632 + $new_password = isset( $_POST['dev_panel_password'] ) ? (string) wp_unslash( $_POST['dev_panel_password'] ) : '';
593 633
594 634 if ( empty( $new_password ) ) {
595 635 wp_send_json_error( array( 'message' => 'Password cannot be empty' ) );
596 636 return;
@@ -600,9 +640,9 @@
600 640 wp_send_json_error( array( 'message' => 'Password must be at least 6 characters long' ) );
601 641 return;
602 642 }
603 643
604 - $result = update_option( self::PASSWORD_OPTION, $new_password );
644 + $result = $this->store_hashed_password( $new_password );
605 645
606 646 if ( $result ) {
607 647 wp_send_json_success( array( 'message' => 'Password updated successfully' ) );
608 648 } else {