| @@ -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) |
| @@ -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 { |
| @@ -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 { |
| @@ -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 |
| @@ -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 { |