| @@ -7,19 +7,20 @@ | ||
| 7 | 7 | |
| 8 | 8 | namespace WP_Defender\Controller; |
| 9 | 9 | |
| 10 | 10 | use Error; |
| 11 | +use WP_User; | |
| 11 | 12 | use Exception; |
| 12 | -use Webauthn\Server; | |
| 13 | +use WP_DEFENDER_VENDOR\Webauthn\Server; | |
| 13 | 14 | use WP_Defender\Controller; |
| 14 | 15 | use WP_Defender\Behavior\WPMUDEV; |
| 15 | -use Nyholm\Psr7\Factory\Psr17Factory; | |
| 16 | -use Webauthn\PublicKeyCredentialRpEntity; | |
| 17 | -use Nyholm\Psr7Server\ServerRequestCreator; | |
| 18 | -use Webauthn\PublicKeyCredentialUserEntity; | |
| 19 | -use Webauthn\AuthenticatorSelectionCriteria; | |
| 20 | -use Webauthn\PublicKeyCredentialRequestOptions; | |
| 21 | -use Webauthn\PublicKeyCredentialCreationOptions; | |
| 16 | +use WP_DEFENDER_VENDOR\Nyholm\Psr7\Factory\Psr17Factory; | |
| 17 | +use WP_DEFENDER_VENDOR\Webauthn\PublicKeyCredentialRpEntity; | |
| 18 | +use WP_DEFENDER_VENDOR\Nyholm\Psr7Server\ServerRequestCreator; | |
| 19 | +use WP_DEFENDER_VENDOR\Webauthn\PublicKeyCredentialUserEntity; | |
| 20 | +use WP_DEFENDER_VENDOR\Webauthn\AuthenticatorSelectionCriteria; | |
| 21 | +use WP_DEFENDER_VENDOR\Webauthn\PublicKeyCredentialRequestOptions; | |
| 22 | +use WP_DEFENDER_VENDOR\Webauthn\PublicKeyCredentialCreationOptions; | |
| 22 | 23 | use WP_Defender\Traits\Webauthn as Webauthn_Trait; |
| 23 | 24 | use WP_Defender\Component\Two_Fa as Two_Fa_Component; |
| 24 | 25 | use WP_Defender\Component\Webauthn as Webauthn_Component; |
| 25 | 26 | use WP_Defender\Controller\Two_Factor as Two_Fa_Controller; |
| @@ -65,10 +66,10 @@ | ||
| 65 | 66 | $this, |
| 66 | 67 | 'get_credential_request_option', |
| 67 | 68 | ) |
| 68 | 69 | ); |
| 69 | - add_action( 'wp_ajax_defender_webauthn_verify_response', array( $this, 'verify_response' ) ); | |
| 70 | - add_action( 'wp_ajax_nopriv_defender_webauthn_verify_response', array( $this, 'verify_response' ) ); | |
| 70 | + add_action( 'wp_ajax_defender_webauthn_verify_response', array( $this, 'handle_verify_response_request' ) ); | |
| 71 | + add_action( 'wp_ajax_nopriv_defender_webauthn_verify_response', array( $this, 'handle_verify_response_request' ) ); | |
| 71 | 72 | // Handling requests in the frontend. |
| 72 | 73 | if ( wd_di()->get( Two_Fa_Controller::class )->woo_integration_enabled() ) { |
| 73 | 74 | add_action( 'wp_ajax_nopriv_defender_webauthn_create_challenge', array( $this, 'create_challenge' ) ); |
| 74 | 75 | add_action( 'wp_ajax_nopriv_defender_webauthn_verify_challenge', array( $this, 'verify_challenge' ) ); |
| @@ -104,9 +105,9 @@ | ||
| 104 | 105 | public function get_current_user_authenticators(): array { |
| 105 | 106 | $arr = array(); |
| 106 | 107 | $user_id = get_current_user_id(); |
| 107 | 108 | $user_credentials = $this->service->getCredentials( $user_id ); |
| 108 | - if ( ! empty( $user_credentials ) && is_array( $user_credentials ) ) { | |
| 109 | + if ( array() !== $user_credentials ) { | |
| 109 | 110 | foreach ( $user_credentials as $key => $value ) { |
| 110 | 111 | $arr[] = array( |
| 111 | 112 | 'key' => $this->base64url_encode( $key ), |
| 112 | 113 | 'label' => $value['label'], |
| @@ -159,9 +160,9 @@ | ||
| 159 | 160 | throw new Exception( esc_html__( 'Bad nonce.', 'defender-security' ) ); |
| 160 | 161 | } |
| 161 | 162 | |
| 162 | 163 | $type = defender_get_data_from_request( 'type', 'g' ); |
| 163 | - if ( empty( $type ) ) { | |
| 164 | + if ( '' === $type ) { | |
| 164 | 165 | throw new Exception( esc_html__( 'Missing field(s).', 'defender-security' ) ); |
| 165 | 166 | } |
| 166 | 167 | |
| 167 | 168 | $user_id = get_current_user_id(); |
| @@ -189,15 +190,12 @@ | ||
| 189 | 190 | $authenticator_type = AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE; |
| 190 | 191 | } |
| 191 | 192 | |
| 192 | 193 | // Create authenticator selection. |
| 193 | - $resident_key = false; | |
| 194 | - $user_verification = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED; | |
| 195 | - $authenticator_selection_criteria = new AuthenticatorSelectionCriteria( | |
| 196 | - $authenticator_type, | |
| 197 | - $resident_key, | |
| 198 | - $user_verification | |
| 199 | - ); | |
| 194 | + $authenticator_selection_criteria = new AuthenticatorSelectionCriteria(); | |
| 195 | + $authenticator_selection_criteria->setAuthenticatorAttachment( $authenticator_type ); | |
| 196 | + $authenticator_selection_criteria->setRequireResidentKey( false ); | |
| 197 | + $authenticator_selection_criteria->setUserVerification( AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED ); | |
| 200 | 198 | |
| 201 | 199 | $rp_entity = new PublicKeyCredentialRpEntity( |
| 202 | 200 | $this->get_site_name(), |
| 203 | 201 | $this->get_site_domain() |
| @@ -246,11 +244,12 @@ | ||
| 246 | 244 | throw new Exception( esc_html__( 'Bad nonce.', 'defender-security' ) ); |
| 247 | 245 | } |
| 248 | 246 | |
| 249 | 247 | $posted_data = defender_get_data_from_request( null, 'p' ); |
| 250 | - if ( empty( $posted_data['data'] ) || empty( $posted_data['client_id'] ) ) { | |
| 248 | + if ( '' === $posted_data['data'] || '' === $posted_data['client_id'] ) { | |
| 251 | 249 | throw new Exception( esc_html__( 'Missing field(s).', 'defender-security' ) ); |
| 252 | 250 | } |
| 251 | + $client_id = sanitize_text_field( $posted_data['client_id'] ); | |
| 253 | 252 | |
| 254 | 253 | $psr17_factory = new Psr17Factory(); |
| 255 | 254 | $creator = new ServerRequestCreator( |
| 256 | 255 | $psr17_factory, |
| @@ -271,12 +270,11 @@ | ||
| 271 | 270 | $this->service, |
| 272 | 271 | null |
| 273 | 272 | ); |
| 274 | 273 | |
| 275 | - $response_data = base64_decode( sanitize_text_field( $posted_data['data'] ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode | |
| 276 | - $client_id = sanitize_text_field( $posted_data['client_id'] ); | |
| 274 | + $response_data = base64_decode( sanitize_text_field( $posted_data['data'] ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode | |
| 277 | 275 | $pub_key_cco = $this->get_trans_val( 'pub_key_cco', $client_id ); |
| 278 | - $pub_key_cco = PublicKeyCredentialCreationOptions::createFromString( base64_decode( $pub_key_cco ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode | |
| 276 | + $pub_key_cco = PublicKeyCredentialCreationOptions::createFromString( base64_decode( $pub_key_cco, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode | |
| 279 | 277 | |
| 280 | 278 | $public_key_credential_source = $server->loadAndCheckAttestationResponse( |
| 281 | 279 | $response_data, |
| 282 | 280 | $pub_key_cco, |
| @@ -307,12 +305,16 @@ | ||
| 307 | 305 | ); |
| 308 | 306 | } |
| 309 | 307 | wp_send_json_success( $response ); |
| 310 | 308 | } catch ( Error $error ) { |
| 311 | - $this->delete_trans( 'pub_key_cco', $client_id ); | |
| 309 | + if ( null !== $client_id ) { | |
| 310 | + $this->delete_trans( 'pub_key_cco', $client_id ); | |
| 311 | + } | |
| 312 | 312 | wp_send_json_error( $error->getMessage() ); |
| 313 | 313 | } catch ( Exception $exception ) { |
| 314 | - $this->delete_trans( 'pub_key_cco', $client_id ); | |
| 314 | + if ( null !== $client_id ) { | |
| 315 | + $this->delete_trans( 'pub_key_cco', $client_id ); | |
| 316 | + } | |
| 315 | 317 | wp_send_json_error( $exception->getMessage() ); |
| 316 | 318 | } |
| 317 | 319 | } |
| 318 | 320 | |
| @@ -328,9 +330,9 @@ | ||
| 328 | 330 | throw new Exception( esc_html__( 'Bad nonce.', 'defender-security' ) ); |
| 329 | 331 | } |
| 330 | 332 | |
| 331 | 333 | $cred_id = defender_get_data_from_request( 'key', 'p' ); |
| 332 | - if ( empty( $cred_id ) ) { | |
| 334 | + if ( '' === $cred_id ) { | |
| 333 | 335 | throw new Exception( esc_html__( 'Missing field(s).', 'defender-security' ) ); |
| 334 | 336 | } |
| 335 | 337 | |
| 336 | 338 | $cred_id = $this->base64url_decode( $cred_id ); |
| @@ -343,9 +345,9 @@ | ||
| 343 | 345 | $this->service->removeUserHandleMatchFailed( $user_id, $cred_id ); |
| 344 | 346 | |
| 345 | 347 | if ( 0 === count( $option_user_credentials ) ) { |
| 346 | 348 | $enabled_providers = get_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, true ); |
| 347 | - if ( empty( $enabled_providers ) ) { | |
| 349 | + if ( ! is_array( $enabled_providers ) ) { | |
| 348 | 350 | $enabled_providers = array(); |
| 349 | 351 | } |
| 350 | 352 | $key = array_search( Webauthn_Provider::$slug, $enabled_providers, true ); |
| 351 | 353 | if ( false !== $key ) { |
| @@ -377,9 +379,9 @@ | ||
| 377 | 379 | throw new Exception( esc_html__( 'Bad nonce.', 'defender-security' ) ); |
| 378 | 380 | } |
| 379 | 381 | |
| 380 | 382 | $posted_data = defender_get_data_from_request( null, 'p' ); |
| 381 | - if ( empty( $posted_data['key'] ) || empty( $posted_data['label'] ) ) { | |
| 383 | + if ( ! isset( $posted_data['key'], $posted_data['label'] ) || '' === $posted_data['key'] || '' === $posted_data['label'] ) { | |
| 382 | 384 | throw new Exception( esc_html__( 'Missing field(s).', 'defender-security' ) ); |
| 383 | 385 | } |
| 384 | 386 | |
| 385 | 387 | $cred_id = sanitize_text_field( $posted_data['key'] ); |
| @@ -415,9 +417,9 @@ | ||
| 415 | 417 | throw new Exception( esc_html__( 'Bad nonce.', 'defender-security' ) ); |
| 416 | 418 | } |
| 417 | 419 | |
| 418 | 420 | $posted_data = defender_get_data_from_request( null, 'p' ); |
| 419 | - if ( empty( $posted_data['username'] ) ) { | |
| 421 | + if ( ! isset( $posted_data['username'] ) || '' === $posted_data['username'] ) { | |
| 420 | 422 | throw new Exception( esc_html__( 'Missing field(s).', 'defender-security' ) ); |
| 421 | 423 | } |
| 422 | 424 | |
| 423 | 425 | $rp_entity = new PublicKeyCredentialRpEntity( |
| @@ -441,9 +443,9 @@ | ||
| 441 | 443 | if ( false === $user_entity ) { |
| 442 | 444 | throw new Exception( esc_html__( 'User does not exist.', 'defender-security' ) ); |
| 443 | 445 | } |
| 444 | 446 | |
| 445 | - $auth_type = ! empty( $posted_data['type'] ) ? sanitize_text_field( $posted_data['type'] ) : null; | |
| 447 | + $auth_type = isset( $posted_data['type'] ) && '' !== $posted_data['type'] ? sanitize_text_field( $posted_data['type'] ) : null; | |
| 446 | 448 | if ( in_array( $auth_type, self::ALLOWED_AUTH_TYPES, true ) ) { |
| 447 | 449 | $credential_sources = $this->service->findAllForUserByType( $user->ID, $auth_type ); |
| 448 | 450 | } else { |
| 449 | 451 | $credential_sources = $this->service->findAllForUserEntity( $user_entity ); |
| @@ -488,16 +490,26 @@ | ||
| 488 | 490 | } |
| 489 | 491 | } |
| 490 | 492 | |
| 491 | 493 | /** |
| 494 | + * Handle verify response request. | |
| 495 | + * | |
| 496 | + * @return void | |
| 497 | + */ | |
| 498 | + public function handle_verify_response_request(): void { | |
| 499 | + $this->verify_response(); | |
| 500 | + } | |
| 501 | + | |
| 502 | + /** | |
| 492 | 503 | * Verify response. |
| 493 | 504 | * |
| 494 | - * @param bool $will_return Either return array or echo json. | |
| 505 | + * @param bool $will_return Either return array or echo json. | |
| 506 | + * @param WP_User|null $expected_user The expected user. | |
| 495 | 507 | * |
| 496 | - * @return array | |
| 508 | + * @return array|void | |
| 497 | 509 | * @throws Exception If something goes wrong. |
| 498 | 510 | */ |
| 499 | - public function verify_response( bool $will_return = false ) { | |
| 511 | + public function verify_response( bool $will_return = false, ?WP_User $expected_user = null ) { | |
| 500 | 512 | $client_id = null; |
| 501 | 513 | try { |
| 502 | 514 | if ( ! $this->verify_nonce( 'wpdef_webauthn', 'post' ) ) { |
| 503 | 515 | throw new Exception( esc_html__( 'Bad nonce.', 'defender-security' ) ); |
| @@ -503,26 +515,52 @@ | ||
| 503 | 515 | throw new Exception( esc_html__( 'Bad nonce.', 'defender-security' ) ); |
| 504 | 516 | } |
| 505 | 517 | |
| 506 | 518 | $posted_data = defender_get_data_from_request( null, 'p' ); |
| 507 | - if ( empty( $posted_data['data'] ) || empty( $posted_data['username'] ) || empty( $posted_data['client_id'] ) ) { | |
| 519 | + if ( | |
| 520 | + ! isset( $posted_data['data'], $posted_data['username'], $posted_data['client_id'] ) | |
| 521 | + || '' === $posted_data['data'] | |
| 522 | + || '' === $posted_data['username'] | |
| 523 | + || '' === $posted_data['client_id'] | |
| 524 | + ) { | |
| 508 | 525 | throw new Exception( esc_html__( 'Missing field(s).', 'defender-security' ) ); |
| 509 | 526 | } |
| 527 | + $client_id = sanitize_text_field( $posted_data['client_id'] ); | |
| 528 | + // Check expected user. | |
| 529 | + if ( $expected_user instanceof WP_User ) { | |
| 530 | + $user = $expected_user; | |
| 531 | + } else { | |
| 532 | + $user = wp_get_current_user(); | |
| 533 | + } | |
| 510 | 534 | |
| 511 | - $user_entity = false; | |
| 512 | - $response_data = base64_decode( $posted_data['data'] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode | |
| 513 | - $username = sanitize_text_field( $posted_data['username'] ); | |
| 514 | - $client_id = sanitize_text_field( $posted_data['client_id'] ); | |
| 515 | - $pub_key_cro = PublicKeyCredentialRequestOptions::createFromString( base64_decode( $this->get_trans_val( 'pub_key_cro', $client_id ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode | |
| 516 | - $user = get_user_by( 'login', $username ); | |
| 535 | + // Establish a trusted scope BEFORE any access to the repository — | |
| 536 | + // both before our own pre-check and before internal library calls | |
| 537 | + // inside loadAndCheckAssertionResponse(). | |
| 538 | + $this->service->set_expected_user_id( $user->ID ); | |
| 539 | + $response_data = base64_decode( $posted_data['data'], true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode | |
| 540 | + if ( false === $response_data ) { | |
| 541 | + throw new Exception( esc_html__( 'Invalid response data.', 'defender-security' ) ); | |
| 542 | + } | |
| 517 | 543 | |
| 518 | - if ( is_object( $user ) ) { | |
| 519 | - $user_entity = $this->get_user_entity( $user->ID ); | |
| 544 | + $decoded_response = json_decode( $response_data, true ); | |
| 545 | + if ( ! is_array( $decoded_response ) || ! isset( $decoded_response['rawId'] ) || ! is_string( $decoded_response['rawId'] ) || '' === $decoded_response['rawId'] ) { | |
| 546 | + throw new Exception( esc_html__( 'Invalid assertion payload.', 'defender-security' ) ); | |
| 520 | 547 | } |
| 521 | 548 | |
| 549 | + $credential_id = $this->base64url_decode( $decoded_response['rawId'] ); | |
| 550 | + if ( false === $credential_id || '' === $credential_id ) { | |
| 551 | + throw new Exception( esc_html__( 'Invalid credential id.', 'defender-security' ) ); | |
| 552 | + } | |
| 553 | + $credential = $this->service->findOneByCredentialId( $credential_id ); | |
| 554 | + if ( ! $credential ) { | |
| 555 | + throw new Exception( esc_html__( 'Invalid user.', 'defender-security' ) ); | |
| 556 | + } | |
| 557 | + | |
| 558 | + $user_entity = $this->get_user_entity( $user->ID ); | |
| 522 | 559 | if ( false === $user_entity ) { |
| 523 | 560 | throw new Exception( esc_html__( 'User does not exist.', 'defender-security' ) ); |
| 524 | 561 | } |
| 562 | + $pub_key_cro = PublicKeyCredentialRequestOptions::createFromString( base64_decode( $this->get_trans_val( 'pub_key_cro', $client_id ), true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode | |
| 525 | 563 | |
| 526 | 564 | $psr17_factory = new Psr17Factory(); |
| 527 | 565 | $creator = new ServerRequestCreator( |
| 528 | 566 | $psr17_factory, |
| @@ -551,14 +589,18 @@ | ||
| 551 | 589 | $server_request |
| 552 | 590 | ); |
| 553 | 591 | |
| 554 | 592 | $this->delete_trans( 'pub_key_cro', $client_id ); |
| 593 | + // Reset the area just in case. | |
| 594 | + $this->service->set_expected_user_id( null ); | |
| 555 | 595 | |
| 556 | 596 | $data = esc_html__( 'Authenticator verified successfully.', 'defender-security' ); |
| 557 | 597 | |
| 558 | 598 | return defender_maybe_echo_json( $data, true, $will_return ); |
| 559 | 599 | } catch ( Error $error ) { |
| 560 | - $this->delete_trans( 'pub_key_cro', $client_id ); | |
| 600 | + if ( null !== $client_id ) { | |
| 601 | + $this->delete_trans( 'pub_key_cro', $client_id ); | |
| 602 | + } | |
| 561 | 603 | |
| 562 | 604 | return defender_maybe_echo_json( $error->getMessage(), false, $will_return ); |
| 563 | 605 | } catch ( Exception $exception ) { |
| 564 | 606 | $data = array( 'message' => $exception->getMessage() ); |
| @@ -572,9 +614,11 @@ | ||
| 572 | 614 | |
| 573 | 615 | $this->service->addUserHandleMatchFailed( $user, $decoded_response ); |
| 574 | 616 | } |
| 575 | 617 | |
| 576 | - $this->delete_trans( 'pub_key_cro', $client_id ); | |
| 618 | + if ( null !== $client_id ) { | |
| 619 | + $this->delete_trans( 'pub_key_cro', $client_id ); | |
| 620 | + } | |
| 577 | 621 | |
| 578 | 622 | return defender_maybe_echo_json( $data, false, $will_return ); |
| 579 | 623 | } |
| 580 | 624 | } |