| @@ -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,12 +66,10 @@ | ||
| 65 | 66 | $this, |
| 66 | 67 | 'get_credential_request_option', |
| 67 | 68 | ) |
| 68 | 69 | ); |
| 69 | - // @phpstan-ignore-next-line | |
| 70 | - add_action( 'wp_ajax_defender_webauthn_verify_response', array( $this, 'verify_response' ) ); | |
| 71 | - // @phpstan-ignore-next-line | |
| 72 | - 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' ) ); | |
| 73 | 72 | // Handling requests in the frontend. |
| 74 | 73 | if ( wd_di()->get( Two_Fa_Controller::class )->woo_integration_enabled() ) { |
| 75 | 74 | add_action( 'wp_ajax_nopriv_defender_webauthn_create_challenge', array( $this, 'create_challenge' ) ); |
| 76 | 75 | add_action( 'wp_ajax_nopriv_defender_webauthn_verify_challenge', array( $this, 'verify_challenge' ) ); |
| @@ -248,8 +247,9 @@ | ||
| 248 | 247 | $posted_data = defender_get_data_from_request( null, 'p' ); |
| 249 | 248 | if ( '' === $posted_data['data'] || '' === $posted_data['client_id'] ) { |
| 250 | 249 | throw new Exception( esc_html__( 'Missing field(s).', 'defender-security' ) ); |
| 251 | 250 | } |
| 251 | + $client_id = sanitize_text_field( $posted_data['client_id'] ); | |
| 252 | 252 | |
| 253 | 253 | $psr17_factory = new Psr17Factory(); |
| 254 | 254 | $creator = new ServerRequestCreator( |
| 255 | 255 | $psr17_factory, |
| @@ -271,9 +271,8 @@ | ||
| 271 | 271 | null |
| 272 | 272 | ); |
| 273 | 273 | |
| 274 | 274 | $response_data = base64_decode( sanitize_text_field( $posted_data['data'] ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode |
| 275 | - $client_id = sanitize_text_field( $posted_data['client_id'] ); | |
| 276 | 275 | $pub_key_cco = $this->get_trans_val( 'pub_key_cco', $client_id ); |
| 277 | 276 | $pub_key_cco = PublicKeyCredentialCreationOptions::createFromString( base64_decode( $pub_key_cco, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode |
| 278 | 277 | |
| 279 | 278 | $public_key_credential_source = $server->loadAndCheckAttestationResponse( |
| @@ -306,12 +305,16 @@ | ||
| 306 | 305 | ); |
| 307 | 306 | } |
| 308 | 307 | wp_send_json_success( $response ); |
| 309 | 308 | } catch ( Error $error ) { |
| 310 | - $this->delete_trans( 'pub_key_cco', $client_id ); | |
| 309 | + if ( null !== $client_id ) { | |
| 310 | + $this->delete_trans( 'pub_key_cco', $client_id ); | |
| 311 | + } | |
| 311 | 312 | wp_send_json_error( $error->getMessage() ); |
| 312 | 313 | } catch ( Exception $exception ) { |
| 313 | - $this->delete_trans( 'pub_key_cco', $client_id ); | |
| 314 | + if ( null !== $client_id ) { | |
| 315 | + $this->delete_trans( 'pub_key_cco', $client_id ); | |
| 316 | + } | |
| 314 | 317 | wp_send_json_error( $exception->getMessage() ); |
| 315 | 318 | } |
| 316 | 319 | } |
| 317 | 320 | |
| @@ -487,16 +490,26 @@ | ||
| 487 | 490 | } |
| 488 | 491 | } |
| 489 | 492 | |
| 490 | 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 | + /** | |
| 491 | 503 | * Verify response. |
| 492 | 504 | * |
| 493 | - * @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. | |
| 494 | 507 | * |
| 495 | 508 | * @return array|void |
| 496 | 509 | * @throws Exception If something goes wrong. |
| 497 | 510 | */ |
| 498 | - public function verify_response( bool $will_return = false ) { | |
| 511 | + public function verify_response( bool $will_return = false, ?WP_User $expected_user = null ) { | |
| 499 | 512 | $client_id = null; |
| 500 | 513 | try { |
| 501 | 514 | if ( ! $this->verify_nonce( 'wpdef_webauthn', 'post' ) ) { |
| 502 | 515 | throw new Exception( esc_html__( 'Bad nonce.', 'defender-security' ) ); |
| @@ -510,23 +523,44 @@ | ||
| 510 | 523 | || '' === $posted_data['client_id'] |
| 511 | 524 | ) { |
| 512 | 525 | throw new Exception( esc_html__( 'Missing field(s).', 'defender-security' ) ); |
| 513 | 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 | + } | |
| 514 | 534 | |
| 515 | - $user_entity = false; | |
| 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 ); | |
| 516 | 539 | $response_data = base64_decode( $posted_data['data'], true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode |
| 517 | - $username = sanitize_text_field( $posted_data['username'] ); | |
| 518 | - $client_id = sanitize_text_field( $posted_data['client_id'] ); | |
| 519 | - $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 | |
| 520 | - $user = get_user_by( 'login', $username ); | |
| 540 | + if ( false === $response_data ) { | |
| 541 | + throw new Exception( esc_html__( 'Invalid response data.', 'defender-security' ) ); | |
| 542 | + } | |
| 521 | 543 | |
| 522 | - if ( is_object( $user ) ) { | |
| 523 | - $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' ) ); | |
| 524 | 547 | } |
| 525 | 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 ); | |
| 526 | 559 | if ( false === $user_entity ) { |
| 527 | 560 | throw new Exception( esc_html__( 'User does not exist.', 'defender-security' ) ); |
| 528 | 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 | |
| 529 | 563 | |
| 530 | 564 | $psr17_factory = new Psr17Factory(); |
| 531 | 565 | $creator = new ServerRequestCreator( |
| 532 | 566 | $psr17_factory, |
| @@ -555,14 +589,18 @@ | ||
| 555 | 589 | $server_request |
| 556 | 590 | ); |
| 557 | 591 | |
| 558 | 592 | $this->delete_trans( 'pub_key_cro', $client_id ); |
| 593 | + // Reset the area just in case. | |
| 594 | + $this->service->set_expected_user_id( null ); | |
| 559 | 595 | |
| 560 | 596 | $data = esc_html__( 'Authenticator verified successfully.', 'defender-security' ); |
| 561 | 597 | |
| 562 | 598 | return defender_maybe_echo_json( $data, true, $will_return ); |
| 563 | 599 | } catch ( Error $error ) { |
| 564 | - $this->delete_trans( 'pub_key_cro', $client_id ); | |
| 600 | + if ( null !== $client_id ) { | |
| 601 | + $this->delete_trans( 'pub_key_cro', $client_id ); | |
| 602 | + } | |
| 565 | 603 | |
| 566 | 604 | return defender_maybe_echo_json( $error->getMessage(), false, $will_return ); |
| 567 | 605 | } catch ( Exception $exception ) { |
| 568 | 606 | $data = array( 'message' => $exception->getMessage() ); |
| @@ -576,9 +614,11 @@ | ||
| 576 | 614 | |
| 577 | 615 | $this->service->addUserHandleMatchFailed( $user, $decoded_response ); |
| 578 | 616 | } |
| 579 | 617 | |
| 580 | - $this->delete_trans( 'pub_key_cro', $client_id ); | |
| 618 | + if ( null !== $client_id ) { | |
| 619 | + $this->delete_trans( 'pub_key_cro', $client_id ); | |
| 620 | + } | |
| 581 | 621 | |
| 582 | 622 | return defender_maybe_echo_json( $data, false, $will_return ); |
| 583 | 623 | } |
| 584 | 624 | } |