← All changes
|
includes/rest-api/class-mainwp-rest-authentication.php
+173
-59
6.1.6
→
6.2.3
View file →
| @@ -113,49 +113,101 @@ | ||
| 113 | 113 | add_filter( 'rest_authentication_errors', array( $this, 'authentication_fallback' ) ); |
| 114 | 114 | add_filter( 'rest_authentication_errors', array( $this, 'check_authentication_error' ), 15 ); |
| 115 | 115 | add_filter( 'rest_post_dispatch', array( $this, 'send_unauthorized_headers' ), 50 ); |
| 116 | 116 | add_filter( 'rest_pre_dispatch', array( $this, 'check_user_permissions' ), 10, 3 ); |
| 117 | + add_action( 'deleted_user', array( $this, 'delete_user_api_keys' ), 10, 3 ); | |
| 118 | + add_action( 'remove_user_from_blog', array( $this, 'remove_user_from_blog_api_keys' ), 10, 3 ); | |
| 119 | + add_action( 'revoked_super_admin', array( $this, 'revoked_super_admin_api_keys' ) ); | |
| 117 | 120 | } |
| 118 | 121 | |
| 119 | 122 | /** |
| 120 | - * Check if this is a request to MainWP's REST API. | |
| 123 | + * Delete an owner's API keys and their cache entries. | |
| 121 | 124 | * |
| 122 | - * This method determines whether the current request should use MainWP's | |
| 123 | - * custom REST API authentication (API keys, OAuth, Bearer tokens). | |
| 125 | + * @param int $id Deleted user ID. | |
| 126 | + * @param int|null $reassign Content reassignment target. API keys are never reassigned. | |
| 127 | + * @param WP_User|null $user Deleted user. | |
| 128 | + */ | |
| 129 | + public function delete_user_api_keys( $id, $reassign = null, $user = null ) { | |
| 130 | + unset( $reassign, $user ); | |
| 131 | + $db = MainWP_DB::instance(); | |
| 132 | + $rows = $db->remove_rest_api_keys_by_user( $id, $db->get_table_name( 'api_keys' ) ); | |
| 133 | + foreach ( $rows as $row ) { | |
| 134 | + self::delete_api_key_cache( $row->consumer_key ); | |
| 135 | + } | |
| 136 | + } | |
| 137 | + | |
| 138 | + /** | |
| 139 | + * Invalidate the row cached for REST API authentication. | |
| 124 | 140 | * |
| 125 | - * Returns TRUE for: | |
| 126 | - * - /wp-json/mainwp/v2/sites (MainWP core REST API) | |
| 127 | - * - /wp-json/mainwp-extension/v1/foo (MainWP extension REST APIs) | |
| 141 | + * @param string $consumer_key Stored consumer key, already hashed with mainwp_api_hash(). | |
| 142 | + */ | |
| 143 | + public static function delete_api_key_cache( $consumer_key ) { | |
| 144 | + wp_cache_delete( self::api_key_cache_key( $consumer_key ) ); | |
| 145 | + } | |
| 146 | + | |
| 147 | + /** | |
| 148 | + * Object-cache key for a key row. | |
| 128 | 149 | * |
| 129 | - * Returns FALSE for: | |
| 130 | - * - /wp-json/wp-abilities/v1/abilities/mainwp/list-sites-v1 (Abilities API) | |
| 131 | - * - /wp-json/wp/v2/posts (WordPress core REST API) | |
| 132 | - * - Any other REST endpoint not starting with 'mainwp/' or 'mainwp-' | |
| 150 | + * @param string $consumer_key Stored consumer key, already hashed with mainwp_api_hash(). | |
| 151 | + * @return string | |
| 152 | + */ | |
| 153 | + private static function api_key_cache_key( $consumer_key ) { | |
| 154 | + return 'mainwp_api_key_' . md5( $consumer_key ); // NOSONAR - MD5 used for cache key generation only, not cryptographic (security) purposes. | |
| 155 | + } | |
| 156 | + | |
| 157 | + /** | |
| 158 | + * Delete API keys only when their owner leaves MainWP's blog. | |
| 133 | 159 | * |
| 134 | - * IMPORTANT: The Abilities API endpoints contain 'mainwp' in their path | |
| 135 | - * (as the ability provider namespace) but MUST use native WordPress | |
| 136 | - * authentication (cookie auth, application passwords, etc.) - NOT MainWP's | |
| 137 | - * API key system. This method's exact prefix matching ensures Abilities | |
| 138 | - * endpoints are excluded. | |
| 160 | + * @param int $user_id Removed user ID. | |
| 161 | + * @param int $blog_id Blog the user is leaving. | |
| 162 | + * @param int $reassign Content reassignment target. API keys are never reassigned. | |
| 163 | + */ | |
| 164 | + public function remove_user_from_blog_api_keys( $user_id, $blog_id, $reassign = 0 ) { | |
| 165 | + global $wpdb; | |
| 166 | + | |
| 167 | + unset( $reassign ); | |
| 168 | + if ( $wpdb->get_blog_prefix( $blog_id ) . 'mainwp_api_keys' !== MainWP_DB::instance()->get_table_name( 'api_keys' ) ) { | |
| 169 | + return; | |
| 170 | + } | |
| 171 | + $this->delete_user_api_keys( $user_id ); | |
| 172 | + } | |
| 173 | + | |
| 174 | + /** | |
| 175 | + * Delete a former super admin's API keys when they are not a member of MainWP's blog. | |
| 139 | 176 | * |
| 177 | + * Super admins may hold keys on a Dashboard subsite they were never added to. Once revoked, those keys | |
| 178 | + * are refused at use time, but would work again if super admin were re-granted. | |
| 179 | + * | |
| 180 | + * @param int $user_id User whose super admin privileges were revoked. | |
| 181 | + */ | |
| 182 | + public function revoked_super_admin_api_keys( $user_id ) { | |
| 183 | + global $wpdb; | |
| 184 | + | |
| 185 | + if ( $wpdb->get_blog_prefix() . 'mainwp_api_keys' !== MainWP_DB::instance()->get_table_name( 'api_keys' ) || is_user_member_of_blog( $user_id ) ) { | |
| 186 | + return; | |
| 187 | + } | |
| 188 | + $this->delete_user_api_keys( $user_id ); | |
| 189 | + } | |
| 190 | + | |
| 191 | + /** | |
| 192 | + * Check whether WordPress is dispatching a MainWP REST route. | |
| 193 | + * | |
| 194 | + * Uses the parsed rest_route query variable, which is available during REST | |
| 195 | + * serving. An empty or unset route means no REST request is being served. | |
| 196 | + * The first path segment must be 'mainwp' or start with 'mainwp-', compared | |
| 197 | + * case-insensitively after trimming and normalising the leading slash. | |
| 198 | + * | |
| 199 | + * Abilities routes such as /wp-abilities/v1/abilities/mainwp/list-sites-v1 | |
| 200 | + * MUST use native WordPress authentication and are excluded. | |
| 201 | + * | |
| 140 | 202 | * @return bool True if request is to MainWP REST API, false otherwise. |
| 141 | 203 | */ |
| 142 | 204 | protected function is_request_to_rest_api() { |
| 143 | - if ( empty( $_SERVER['REQUEST_URI'] ) ) { | |
| 205 | + $route = $GLOBALS['wp']->query_vars['rest_route'] ?? ''; | |
| 206 | + if ( empty( $route ) || ! is_string( $route ) ) { | |
| 144 | 207 | return false; |
| 145 | 208 | } |
| 146 | 209 | |
| 147 | - $rest_prefix = trailingslashit( rest_get_url_prefix() ); | |
| 148 | - $request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ); | |
| 149 | - | |
| 150 | - // Check if the request is to the MainWP API namespace. | |
| 151 | - // Use strpos with exact prefix to avoid false positives from other APIs | |
| 152 | - // that may have 'mainwp/' in their URL path (e.g., wp-abilities/v1/abilities/mainwp/...). | |
| 153 | - $mainwp_api = ( false !== strpos( $request_uri, '/' . $rest_prefix . self::MAINWP_REST_NAMESPACE ) ); | |
| 154 | - | |
| 155 | - // Allow third party plugins use our authentication methods. | |
| 156 | - $extension_api = ( false !== strpos( $request_uri, '/' . $rest_prefix . self::MAINWP_EXTENSION_REST_NAMESPACE ) ); | |
| 157 | - | |
| 158 | 210 | /** |
| 159 | 211 | * Filter whether a request is considered to be to the MainWP REST API. |
| 160 | 212 | * |
| 161 | 213 | * IMPORTANT: Do NOT add Abilities API endpoints (wp-abilities/v1/abilities/mainwp/...) |
| @@ -163,12 +215,25 @@ | ||
| 163 | 215 | * NOT be processed by MainWP's REST authentication system. |
| 164 | 216 | * |
| 165 | 217 | * @param bool $is_mainwp_api True if this is a MainWP REST API request. |
| 166 | 218 | */ |
| 167 | - return apply_filters( 'mainwp_rest_is_request_to_rest_api', $mainwp_api || $extension_api ); | |
| 219 | + return apply_filters( 'mainwp_rest_is_request_to_rest_api', $this->is_mainwp_route( $route ) ); | |
| 168 | 220 | } |
| 169 | 221 | |
| 170 | 222 | /** |
| 223 | + * Match the first route segment to a MainWP namespace. | |
| 224 | + * | |
| 225 | + * @param string $route REST route to classify. | |
| 226 | + * @return bool | |
| 227 | + */ | |
| 228 | + private function is_mainwp_route( string $route ) { | |
| 229 | + $route = '/' . ltrim( trim( $route ), '/' ); | |
| 230 | + $namespace = strtolower( explode( '/', $route )[1] ); | |
| 231 | + | |
| 232 | + return rtrim( self::MAINWP_REST_NAMESPACE, '/' ) === $namespace || 0 === strpos( $namespace, self::MAINWP_EXTENSION_REST_NAMESPACE ); | |
| 233 | + } | |
| 234 | + | |
| 235 | + /** | |
| 171 | 236 | * Authenticate user. |
| 172 | 237 | * |
| 173 | 238 | * @param int|false $user_id User ID if one has been determined, false otherwise. |
| 174 | 239 | * @return int|false |
| @@ -280,12 +345,31 @@ | ||
| 280 | 345 | if ( ! $this->is_request_to_rest_api() ) { |
| 281 | 346 | return $error; |
| 282 | 347 | } |
| 283 | 348 | |
| 349 | + // mainwp/v1 routes check their own option-stored v1 keys in each callback and never | |
| 350 | + // read the key user, so a failed v2 attempt there must look like no v2 key was sent. | |
| 351 | + if ( $this->is_v1_route() ) { | |
| 352 | + return $error; | |
| 353 | + } | |
| 354 | + | |
| 284 | 355 | return $this->get_error(); |
| 285 | 356 | } |
| 286 | 357 | |
| 287 | 358 | /** |
| 359 | + * Check whether WordPress is dispatching a mainwp/v1 route. | |
| 360 | + * | |
| 361 | + * Route matching in WP_REST_Server is case-insensitive, so the comparison is too. | |
| 362 | + * | |
| 363 | + * @return bool | |
| 364 | + */ | |
| 365 | + private function is_v1_route() { | |
| 366 | + $segments = explode( '/', strtolower( trim( trim( (string) ( $GLOBALS['wp']->query_vars['rest_route'] ?? '' ) ), '/' ) ) ); | |
| 367 | + | |
| 368 | + return 'mainwp' === $segments[0] && isset( $segments[1] ) && 'v1' === $segments[1]; | |
| 369 | + } | |
| 370 | + | |
| 371 | + /** | |
| 288 | 372 | * Set authentication error. |
| 289 | 373 | * |
| 290 | 374 | * @param WP_Error $error Authentication error data. |
| 291 | 375 | */ |
| @@ -305,8 +389,19 @@ | ||
| 305 | 389 | return $this->error; |
| 306 | 390 | } |
| 307 | 391 | |
| 308 | 392 | /** |
| 393 | + * Error for a submitted key that is unknown, disabled, ownerless or paired with the wrong secret. | |
| 394 | + * | |
| 395 | + * One response for all of them, so a caller holding only a consumer key cannot tell whether it exists. | |
| 396 | + * | |
| 397 | + * @return WP_Error | |
| 398 | + */ | |
| 399 | + private function refused_key_error() { | |
| 400 | + return new WP_Error( 'mainwp_rest_authentication_disabled_key', __( 'The REST API Key are disabled.', 'mainwp' ), array( 'status' => 401 ) ); | |
| 401 | + } | |
| 402 | + | |
| 403 | + /** | |
| 309 | 404 | * Detect whether a stored consumer_secret value is still in legacy plaintext format. |
| 310 | 405 | * |
| 311 | 406 | * Legacy v2 keys were stored as `cs_` followed by hex digits (production uses |
| 312 | 407 | * 40 hex chars; some older fixtures use 32). New keys created after MWP-1540 |
| @@ -337,8 +432,12 @@ | ||
| 337 | 432 | * @param string $submitted Consumer_secret submitted in the request. |
| 338 | 433 | * @return bool True if the submitted secret matches the stored value. |
| 339 | 434 | */ |
| 340 | 435 | private function verify_consumer_secret( $stored, $submitted ) { |
| 436 | + // An array secret (consumer_secret[]=x) would throw here, and only for a live key, which reveals that it exists. | |
| 437 | + if ( ! is_string( $submitted ) ) { | |
| 438 | + return false; | |
| 439 | + } | |
| 341 | 440 | if ( $this->is_legacy_plaintext_secret( $stored ) ) { |
| 342 | 441 | return hash_equals( $stored, $submitted ); // @codingStandardsIgnoreLine |
| 343 | 442 | } |
| 344 | 443 | return wp_check_password( $submitted, $stored ); |
| @@ -378,17 +477,18 @@ | ||
| 378 | 477 | return false; |
| 379 | 478 | } |
| 380 | 479 | |
| 381 | 480 | // Get user data. |
| 382 | - $this->user = $this->get_user_data_by_consumer_key( $consumer_key ); | |
| 481 | + $user = $this->get_user_data_by_consumer_key( $consumer_key ); | |
| 383 | 482 | |
| 384 | - if ( empty( $this->user ) ) { | |
| 483 | + if ( empty( $user ) ) { | |
| 385 | 484 | return false; |
| 386 | 485 | } |
| 486 | + $this->user = $user; | |
| 387 | 487 | |
| 388 | 488 | // Validate user secret. Handles both legacy plaintext rows and hashed rows. |
| 389 | 489 | if ( ! $this->verify_consumer_secret( $this->user->consumer_secret, $consumer_secret ) ) { |
| 390 | - $this->set_error( new WP_Error( 'mainwp_rest_authentication_error', __( 'Consumer secret is invalid.', 'mainwp' ), array( 'status' => 401 ) ) ); | |
| 490 | + $this->set_error( $this->refused_key_error() ); | |
| 391 | 491 | |
| 392 | 492 | return false; |
| 393 | 493 | } |
| 394 | 494 | |
| @@ -558,16 +658,20 @@ | ||
| 558 | 658 | if ( empty( $params ) ) { |
| 559 | 659 | return false; |
| 560 | 660 | } |
| 561 | 661 | |
| 662 | + if ( ! in_array( $params['oauth_signature_method'], array( 'HMAC-SHA1', 'HMAC-SHA256' ), true ) ) { | |
| 663 | + $this->set_error( new WP_Error( 'mainwp_rest_authentication_error', __( 'Invalid signature - signature method is invalid.', 'mainwp' ), array( 'status' => 401 ) ) ); | |
| 664 | + return false; | |
| 665 | + } | |
| 666 | + | |
| 562 | 667 | // Fetch WP user by consumer key. |
| 563 | - $this->user = $this->get_user_data_by_consumer_key( $params['oauth_consumer_key'] ); | |
| 668 | + $user = $this->get_user_data_by_consumer_key( $params['oauth_consumer_key'] ); | |
| 564 | 669 | |
| 565 | - if ( empty( $this->user ) ) { | |
| 566 | - $this->set_error( new WP_Error( 'mainwp_rest_authentication_error', __( 'Consumer key is invalid.', 'mainwp' ), array( 'status' => 401 ) ) ); | |
| 567 | - | |
| 670 | + if ( empty( $user ) ) { | |
| 568 | 671 | return false; |
| 569 | 672 | } |
| 673 | + $this->user = $user; | |
| 570 | 674 | |
| 571 | 675 | // Perform OAuth validation. |
| 572 | 676 | $signature = $this->check_oauth_signature( $this->user, $params ); |
| 573 | 677 | if ( is_wp_error( $signature ) ) { |
| @@ -618,15 +722,16 @@ | ||
| 618 | 722 | if ( ! $consumer_key || ! $consumer_secret ) { |
| 619 | 723 | return false; |
| 620 | 724 | } |
| 621 | 725 | // Get user data. |
| 622 | - $this->user = $this->get_user_data_by_consumer_key( $consumer_key ); | |
| 623 | - if ( empty( $this->user ) ) { | |
| 726 | + $user = $this->get_user_data_by_consumer_key( $consumer_key ); | |
| 727 | + if ( empty( $user ) ) { | |
| 624 | 728 | return false; |
| 625 | 729 | } |
| 730 | + $this->user = $user; | |
| 626 | 731 | // Validate user secret. Handles both legacy plaintext rows and hashed rows. |
| 627 | 732 | if ( ! $this->verify_consumer_secret( $this->user->consumer_secret, $consumer_secret ) ) { |
| 628 | - $this->set_error( new WP_Error( 'mainwp_rest_authentication_error', __( 'Consumer secret is invalid.', 'mainwp' ), array( 'status' => 401 ) ) ); | |
| 733 | + $this->set_error( $this->refused_key_error() ); | |
| 629 | 734 | |
| 630 | 735 | return false; |
| 631 | 736 | } |
| 632 | 737 | return $this->user->user_id; |
| @@ -706,8 +811,13 @@ | ||
| 706 | 811 | $request_path = substr( $request_path, strlen( $wp_base ) ); |
| 707 | 812 | } |
| 708 | 813 | $base_request_uri = rawurlencode( get_home_url( null, $request_path, $this->is_ssl_request() ? 'https' : 'http' ) ); |
| 709 | 814 | |
| 815 | + // Same reasoning as verify_consumer_secret(): an array signature must not throw only for a live key. | |
| 816 | + if ( ! is_string( $params['oauth_signature'] ) ) { | |
| 817 | + return $this->refused_key_error(); | |
| 818 | + } | |
| 819 | + | |
| 710 | 820 | // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature. |
| 711 | 821 | $consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) ); |
| 712 | 822 | unset( $params['oauth_signature'] ); |
| 713 | 823 | |
| @@ -720,23 +830,12 @@ | ||
| 720 | 830 | $params = $this->normalize_parameters( $params ); |
| 721 | 831 | $query_string = implode( '%26', $this->join_with_equals_sign( $params ) ); // Join with ampersand. |
| 722 | 832 | $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; |
| 723 | 833 | |
| 724 | - if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) { | |
| 725 | - return new WP_Error( 'mainwp_rest_authentication_error', __( 'Invalid signature - signature method is invalid.', 'mainwp' ), array( 'status' => 401 ) ); | |
| 726 | - } | |
| 727 | - | |
| 728 | - // OAuth 1.0a HMAC verification requires the plaintext secret as the HMAC | |
| 729 | - // key. Keys created after MWP-1540 store the secret hashed at rest and | |
| 730 | - // therefore cannot be used with this auth method. Direct the caller to | |
| 731 | - // Basic Auth over HTTPS or to reissue (and use the new key only via | |
| 732 | - // Basic Auth). | |
| 733 | - // | |
| 734 | - // TODO MWP-1540 sunset (~v6.3): drop this branch. By the sunset target | |
| 735 | - // any plaintext keys still in the wild will already be failing through | |
| 736 | - // Basic Auth as well, so reissue is the only path forward anyway. | |
| 834 | + // HMAC needs the plaintext secret. A distinct error for a hashed secret | |
| 835 | + // would reveal that the submitted consumer key exists. | |
| 737 | 836 | if ( ! $this->is_legacy_plaintext_secret( $user->consumer_secret ) ) { |
| 738 | - return new WP_Error( 'mainwp_rest_authentication_oauth1_unsupported', __( 'OAuth 1.0a is not supported for this API key. Use Basic Auth over HTTPS or reissue the key.', 'mainwp' ), array( 'status' => 401 ) ); | |
| 837 | + return $this->refused_key_error(); | |
| 739 | 838 | } |
| 740 | 839 | |
| 741 | 840 | $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); |
| 742 | 841 | $secret = $user->consumer_secret . '&'; |
| @@ -742,9 +841,9 @@ | ||
| 742 | 841 | $secret = $user->consumer_secret . '&'; |
| 743 | 842 | $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); //phpcs:ignore -- ok. |
| 744 | 843 | |
| 745 | 844 | if ( ! hash_equals( $signature, $consumer_signature ) ) { // @codingStandardsIgnoreLine |
| 746 | - return new WP_Error( 'mainwp_rest_authentication_error', __( 'Invalid signature - provided signature does not match.', 'mainwp' ), array( 'status' => 401 ) ); | |
| 845 | + return $this->refused_key_error(); | |
| 747 | 846 | } |
| 748 | 847 | return true; |
| 749 | 848 | } |
| 750 | 849 | |
| @@ -846,9 +945,9 @@ | ||
| 846 | 945 | array( '%s' ), |
| 847 | 946 | array( '%d' ) |
| 848 | 947 | ); |
| 849 | 948 | |
| 850 | - wp_cache_delete( 'mainwp_api_key_' . md5( $user->consumer_key ) ); // NOSONAR - MD5 used for cache key generation only, not cryptographic (security) purposes. | |
| 949 | + self::delete_api_key_cache( $user->consumer_key ); | |
| 851 | 950 | |
| 852 | 951 | return true; |
| 853 | 952 | } |
| 854 | 953 | |
| @@ -861,9 +960,9 @@ | ||
| 861 | 960 | private function get_user_data_by_consumer_key( $consumer_key ) { |
| 862 | 961 | global $wpdb; |
| 863 | 962 | |
| 864 | 963 | $consumer_key = \mainwp_api_hash( sanitize_text_field( $consumer_key ) ); |
| 865 | - $cache_key = 'mainwp_api_key_' . md5( $consumer_key ); // NOSONAR - MD5 used for cache key generation only, not cryptographic (security) purposes. | |
| 964 | + $cache_key = self::api_key_cache_key( $consumer_key ); | |
| 866 | 965 | $user = wp_cache_get( $cache_key ); |
| 867 | 966 | |
| 868 | 967 | if ( false === $user ) { |
| 869 | 968 | $table_name = esc_sql( MainWP_DB::instance()->get_table_name( 'api_keys' ) ); |
| @@ -873,17 +972,28 @@ | ||
| 873 | 972 | $consumer_key |
| 874 | 973 | ) |
| 875 | 974 | ); |
| 876 | 975 | if ( $user ) { |
| 877 | - wp_cache_set( $cache_key, $user ); | |
| 976 | + // Key edits and deletes clear this entry, but a request that read the row just before the write can | |
| 977 | + // store it again afterwards; the expiry bounds how long that stale row can authenticate. | |
| 978 | + wp_cache_set( $cache_key, $user, '', 5 * MINUTE_IN_SECONDS ); | |
| 878 | 979 | } |
| 879 | 980 | } |
| 880 | 981 | |
| 881 | 982 | if ( empty( $user->enabled ) ) { |
| 882 | - $this->set_error( new \WP_Error( 'mainwp_rest_authentication_disabled_key', __( 'The REST API Key are disabled.', 'mainwp' ), array( 'status' => 401 ) ) ); | |
| 983 | + $this->set_error( $this->refused_key_error() ); | |
| 883 | 984 | return false; |
| 884 | 985 | } |
| 885 | 986 | |
| 987 | + $user_id = (int) $user->user_id; | |
| 988 | + $user_exists = false !== get_userdata( $user_id ); | |
| 989 | + // Super admins can hold keys on a Dashboard subsite they were never added to. | |
| 990 | + $outside_blog = is_multisite() && ! is_super_admin( $user_id ) && ! is_user_member_of_blog( $user_id ); | |
| 991 | + if ( ! $user_exists || $outside_blog ) { | |
| 992 | + $this->set_error( $this->refused_key_error() ); | |
| 993 | + return false; | |
| 994 | + } | |
| 995 | + | |
| 886 | 996 | // Legacy: passphrase enforcement for any rows where key_type=1. |
| 887 | 997 | // No current code path creates such rows (see MWP-1544) and no caller |
| 888 | 998 | // of insert_rest_api_key sets key_type to 1, so this branch is dormant |
| 889 | 999 | // on every install we have visibility into. It is preserved to avoid |
| @@ -903,9 +1013,9 @@ | ||
| 903 | 1013 | // query, so sanitization is intentionally skipped to preserve |
| 904 | 1014 | // an exact match against legacy plaintext values that may |
| 905 | 1015 | // contain whitespace or HTML-like characters. |
| 906 | 1016 | // phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 907 | - $pass = ! empty( $_REQUEST['key_pass'] ) ? wp_unslash( $_REQUEST['key_pass'] ) : ''; | |
| 1017 | + $pass = ! empty( $_REQUEST['key_pass'] ) && is_string( $_REQUEST['key_pass'] ) ? wp_unslash( $_REQUEST['key_pass'] ) : ''; | |
| 908 | 1018 | // phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 909 | 1019 | $expected = isset( $user->key_pass ) ? (string) $user->key_pass : ''; |
| 910 | 1020 | if ( '' === $expected || ! hash_equals( $expected, $pass ) ) { |
| 911 | 1021 | $this->set_error( new \WP_Error( 'mainwp_rest_authentication_invalid_key_pass', __( 'The REST API passphrase is invalid.', 'mainwp' ), array( 'status' => 401 ) ) ); |
| @@ -987,9 +1097,9 @@ | ||
| 987 | 1097 | array( '%s' ), |
| 988 | 1098 | array( '%d' ) |
| 989 | 1099 | ); |
| 990 | 1100 | |
| 991 | - wp_cache_delete( 'mainwp_api_key_' . md5( $this->user->consumer_key ) ); // NOSONAR - MD5 used for cache key generation only, not cryptographic (security) purposes. | |
| 1101 | + self::delete_api_key_cache( $this->user->consumer_key ); | |
| 992 | 1102 | } |
| 993 | 1103 | |
| 994 | 1104 | /** |
| 995 | 1105 | * If the consumer_key and consumer_secret $_GET parameters are NOT provided |
| @@ -1018,8 +1128,12 @@ | ||
| 1018 | 1128 | */ |
| 1019 | 1129 | public function check_user_permissions( $result, $server, $request ) { |
| 1020 | 1130 | unset( $server ); |
| 1021 | 1131 | if ( $this->user ) { |
| 1132 | + if ( ! $this->is_mainwp_route( $request->get_route() ) ) { | |
| 1133 | + return new WP_Error( 'mainwp_rest_authentication_error', __( 'MainWP API keys can only authenticate MainWP REST routes.', 'mainwp' ), array( 'status' => 401 ) ); | |
| 1134 | + } | |
| 1135 | + | |
| 1022 | 1136 | // Check API Key permissions. |
| 1023 | 1137 | $allowed = $this->check_permissions( $request->get_method() ); |
| 1024 | 1138 | if ( is_wp_error( $allowed ) ) { |
| 1025 | 1139 | return $allowed; |