| @@ -7,8 +7,9 @@ | ||
| 7 | 7 | |
| 8 | 8 | namespace WP_Defender\Controller; |
| 9 | 9 | |
| 10 | 10 | use Error; |
| 11 | +use WP_User; | |
| 11 | 12 | use Exception; |
| 12 | 13 | use WP_DEFENDER_VENDOR\Webauthn\Server; |
| 13 | 14 | use WP_Defender\Controller; |
| 14 | 15 | use WP_Defender\Behavior\WPMUDEV; |
| @@ -246,8 +247,9 @@ | ||
| 246 | 247 | $posted_data = defender_get_data_from_request( null, 'p' ); |
| 247 | 248 | if ( '' === $posted_data['data'] || '' === $posted_data['client_id'] ) { |
| 248 | 249 | throw new Exception( esc_html__( 'Missing field(s).', 'defender-security' ) ); |
| 249 | 250 | } |
| 251 | + $client_id = sanitize_text_field( $posted_data['client_id'] ); | |
| 250 | 252 | |
| 251 | 253 | $psr17_factory = new Psr17Factory(); |
| 252 | 254 | $creator = new ServerRequestCreator( |
| 253 | 255 | $psr17_factory, |
| @@ -269,9 +271,8 @@ | ||
| 269 | 271 | null |
| 270 | 272 | ); |
| 271 | 273 | |
| 272 | 274 | $response_data = base64_decode( sanitize_text_field( $posted_data['data'] ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode |
| 273 | - $client_id = sanitize_text_field( $posted_data['client_id'] ); | |
| 274 | 275 | $pub_key_cco = $this->get_trans_val( 'pub_key_cco', $client_id ); |
| 275 | 276 | $pub_key_cco = PublicKeyCredentialCreationOptions::createFromString( base64_decode( $pub_key_cco, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode |
| 276 | 277 | |
| 277 | 278 | $public_key_credential_source = $server->loadAndCheckAttestationResponse( |
| @@ -304,12 +305,16 @@ | ||
| 304 | 305 | ); |
| 305 | 306 | } |
| 306 | 307 | wp_send_json_success( $response ); |
| 307 | 308 | } catch ( Error $error ) { |
| 308 | - $this->delete_trans( 'pub_key_cco', $client_id ); | |
| 309 | + if ( null !== $client_id ) { | |
| 310 | + $this->delete_trans( 'pub_key_cco', $client_id ); | |
| 311 | + } | |
| 309 | 312 | wp_send_json_error( $error->getMessage() ); |
| 310 | 313 | } catch ( Exception $exception ) { |
| 311 | - $this->delete_trans( 'pub_key_cco', $client_id ); | |
| 314 | + if ( null !== $client_id ) { | |
| 315 | + $this->delete_trans( 'pub_key_cco', $client_id ); | |
| 316 | + } | |
| 312 | 317 | wp_send_json_error( $exception->getMessage() ); |
| 313 | 318 | } |
| 314 | 319 | } |
| 315 | 320 | |
| @@ -496,14 +501,15 @@ | ||
| 496 | 501 | |
| 497 | 502 | /** |
| 498 | 503 | * Verify response. |
| 499 | 504 | * |
| 500 | - * @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. | |
| 501 | 507 | * |
| 502 | 508 | * @return array|void |
| 503 | 509 | * @throws Exception If something goes wrong. |
| 504 | 510 | */ |
| 505 | - public function verify_response( bool $will_return = false ) { | |
| 511 | + public function verify_response( bool $will_return = false, ?WP_User $expected_user = null ) { | |
| 506 | 512 | $client_id = null; |
| 507 | 513 | try { |
| 508 | 514 | if ( ! $this->verify_nonce( 'wpdef_webauthn', 'post' ) ) { |
| 509 | 515 | throw new Exception( esc_html__( 'Bad nonce.', 'defender-security' ) ); |
| @@ -517,23 +523,44 @@ | ||
| 517 | 523 | || '' === $posted_data['client_id'] |
| 518 | 524 | ) { |
| 519 | 525 | throw new Exception( esc_html__( 'Missing field(s).', 'defender-security' ) ); |
| 520 | 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 | + } | |
| 521 | 534 | |
| 522 | - $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 ); | |
| 523 | 539 | $response_data = base64_decode( $posted_data['data'], true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode |
| 524 | - $username = sanitize_text_field( $posted_data['username'] ); | |
| 525 | - $client_id = sanitize_text_field( $posted_data['client_id'] ); | |
| 526 | - $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 | |
| 527 | - $user = get_user_by( 'login', $username ); | |
| 540 | + if ( false === $response_data ) { | |
| 541 | + throw new Exception( esc_html__( 'Invalid response data.', 'defender-security' ) ); | |
| 542 | + } | |
| 528 | 543 | |
| 529 | - if ( is_object( $user ) ) { | |
| 530 | - $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' ) ); | |
| 531 | 547 | } |
| 532 | 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 ); | |
| 533 | 559 | if ( false === $user_entity ) { |
| 534 | 560 | throw new Exception( esc_html__( 'User does not exist.', 'defender-security' ) ); |
| 535 | 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 | |
| 536 | 563 | |
| 537 | 564 | $psr17_factory = new Psr17Factory(); |
| 538 | 565 | $creator = new ServerRequestCreator( |
| 539 | 566 | $psr17_factory, |
| @@ -562,14 +589,18 @@ | ||
| 562 | 589 | $server_request |
| 563 | 590 | ); |
| 564 | 591 | |
| 565 | 592 | $this->delete_trans( 'pub_key_cro', $client_id ); |
| 593 | + // Reset the area just in case. | |
| 594 | + $this->service->set_expected_user_id( null ); | |
| 566 | 595 | |
| 567 | 596 | $data = esc_html__( 'Authenticator verified successfully.', 'defender-security' ); |
| 568 | 597 | |
| 569 | 598 | return defender_maybe_echo_json( $data, true, $will_return ); |
| 570 | 599 | } catch ( Error $error ) { |
| 571 | - $this->delete_trans( 'pub_key_cro', $client_id ); | |
| 600 | + if ( null !== $client_id ) { | |
| 601 | + $this->delete_trans( 'pub_key_cro', $client_id ); | |
| 602 | + } | |
| 572 | 603 | |
| 573 | 604 | return defender_maybe_echo_json( $error->getMessage(), false, $will_return ); |
| 574 | 605 | } catch ( Exception $exception ) { |
| 575 | 606 | $data = array( 'message' => $exception->getMessage() ); |
| @@ -583,9 +614,11 @@ | ||
| 583 | 614 | |
| 584 | 615 | $this->service->addUserHandleMatchFailed( $user, $decoded_response ); |
| 585 | 616 | } |
| 586 | 617 | |
| 587 | - $this->delete_trans( 'pub_key_cro', $client_id ); | |
| 618 | + if ( null !== $client_id ) { | |
| 619 | + $this->delete_trans( 'pub_key_cro', $client_id ); | |
| 620 | + } | |
| 588 | 621 | |
| 589 | 622 | return defender_maybe_echo_json( $data, false, $will_return ); |
| 590 | 623 | } |
| 591 | 624 | } |