| @@ -7,12 +7,15 @@ | ||
| 7 | 7 | |
| 8 | 8 | namespace WP_Defender\Component; |
| 9 | 9 | |
| 10 | 10 | use WP_User; |
| 11 | -use Webauthn\PublicKeyCredentialSource; | |
| 12 | -use Webauthn\PublicKeyCredentialUserEntity; | |
| 11 | +use WP_DEFENDER_VENDOR\Webauthn\PublicKeyCredentialSource; | |
| 12 | +use WP_DEFENDER_VENDOR\Webauthn\PublicKeyCredentialUserEntity; | |
| 13 | +use WP_DEFENDER_VENDOR\Webauthn\TrustPath\CertificateTrustPath; | |
| 14 | +use WP_DEFENDER_VENDOR\Webauthn\TrustPath\EcdaaKeyIdTrustPath; | |
| 15 | +use WP_DEFENDER_VENDOR\Webauthn\TrustPath\EmptyTrustPath; | |
| 13 | 16 | use WP_Defender\Traits\Webauthn as Webauthn_Trait; |
| 14 | -use Webauthn\PublicKeyCredentialSourceRepository; | |
| 17 | +use WP_DEFENDER_VENDOR\Webauthn\PublicKeyCredentialSourceRepository; | |
| 15 | 18 | |
| 16 | 19 | /** |
| 17 | 20 | * Handles WebAuthn functionalities providing methods to manage and verify user credentials. |
| 18 | 21 | * |
| @@ -36,8 +39,31 @@ | ||
| 36 | 39 | */ |
| 37 | 40 | public const USER_HANDLE_MISMATCH_KEY = 'user_handle_match_failed'; |
| 38 | 41 | |
| 39 | 42 | /** |
| 43 | + * User ID that findOneByCredentialId() lookups are currently scoped to. | |
| 44 | + * Must be set only via set_expected_user_id() from trusted server-side | |
| 45 | + * context (e.g. the WP_User passed down the 2FA login chain) — never | |
| 46 | + * derived from request input inside this class. | |
| 47 | + * | |
| 48 | + * @var int|null | |
| 49 | + */ | |
| 50 | + private $expected_user_id = null; | |
| 51 | + | |
| 52 | + /** | |
| 53 | + * Restrict findOneByCredentialId() to a specific user's credential set. | |
| 54 | + * Call this before delegating to Webauthn\Server so the library's internal | |
| 55 | + * repository lookups stay scoped to the expected 2FA target user. | |
| 56 | + * | |
| 57 | + * @param int|null $user_id Trusted user ID, or null to clear the scope. | |
| 58 | + * | |
| 59 | + * @return void | |
| 60 | + */ | |
| 61 | + public function set_expected_user_id( ?int $user_id ): void { | |
| 62 | + $this->expected_user_id = $user_id; | |
| 63 | + } | |
| 64 | + | |
| 65 | + /** | |
| 40 | 66 | * Get user credentials. |
| 41 | 67 | * |
| 42 | 68 | * @param int $user_id The user ID. |
| 43 | 69 | * |
| @@ -60,8 +86,12 @@ | ||
| 60 | 86 | } |
| 61 | 87 | |
| 62 | 88 | /** |
| 63 | 89 | * Get one credential by credential ID. |
| 90 | + * {@inheritDoc} | |
| 91 | + * Interface-mandated single-parameter signature. Scoping to a specific | |
| 92 | + * user is done via $this->expected_user_id, set beforehand through | |
| 93 | + * set_expected_user_id() by trusted calling code — never via $_POST. | |
| 64 | 94 | * |
| 65 | 95 | * @param string $public_key_credential_id The public key credential ID. |
| 66 | 96 | * |
| 67 | 97 | * @return PublicKeyCredentialSource|null |
| @@ -66,24 +96,21 @@ | ||
| 66 | 96 | * |
| 67 | 97 | * @return PublicKeyCredentialSource|null |
| 68 | 98 | */ |
| 69 | 99 | public function findOneByCredentialId( string $public_key_credential_id ): ?PublicKeyCredentialSource { |
| 70 | - $username = defender_get_data_from_request( 'username', 'p' ); | |
| 71 | - if ( isset( $username ) ) { | |
| 72 | - $user = get_user_by( 'login', $username ); | |
| 100 | + if ( null === $this->expected_user_id ) { | |
| 101 | + // No trusted scope set — deny rather than search globally. | |
| 102 | + // Never needs an unscoped, cross-account lookup. | |
| 103 | + return null; | |
| 104 | + } | |
| 73 | 105 | |
| 74 | - if ( is_object( $user ) ) { | |
| 75 | - $user_id = $user->ID; | |
| 76 | - } else { | |
| 77 | - $user_id = 0; | |
| 78 | - } | |
| 79 | - } else { | |
| 80 | - $user_id = get_current_user_id(); | |
| 106 | + $data = $this->getCredentials( $this->expected_user_id ); | |
| 107 | + $key = base64_encode( $public_key_credential_id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode | |
| 108 | + if ( isset( $data[ $key ]['credential_source'] ) ) { | |
| 109 | + $credential_source = $data[ $key ]['credential_source']; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode | |
| 110 | + | |
| 111 | + return PublicKeyCredentialSource::createFromArray( $this->normalize_legacy_trust_path( $credential_source ) ); | |
| 81 | 112 | } |
| 82 | - $data = $this->getCredentials( $user_id ); | |
| 83 | - if ( isset( $data[ base64_encode( $public_key_credential_id ) ]['credential_source'] ) ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode | |
| 84 | - return PublicKeyCredentialSource::createFromArray( $data[ base64_encode( $public_key_credential_id ) ]['credential_source'] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode | |
| 85 | - } | |
| 86 | 113 | |
| 87 | 114 | return null; |
| 88 | 115 | } |
| 89 | 116 | |
| @@ -130,14 +157,38 @@ | ||
| 130 | 157 | continue; |
| 131 | 158 | } |
| 132 | 159 | |
| 133 | 160 | if ( isset( $data['credential_source'] ) ) { |
| 134 | - $sources[] = PublicKeyCredentialSource::createFromArray( $data['credential_source'] ); | |
| 161 | + $sources[] = PublicKeyCredentialSource::createFromArray( | |
| 162 | + $this->normalize_legacy_trust_path( $data['credential_source'] ) | |
| 163 | + ); | |
| 135 | 164 | } |
| 136 | 165 | } |
| 137 | 166 | } |
| 138 | 167 | |
| 139 | 168 | return $sources; |
| 169 | + } | |
| 170 | + | |
| 171 | + /** | |
| 172 | + * Map trust-path class names stored by Defender 6.1 to their Mozart-prefixed 6.2 classes. | |
| 173 | + * | |
| 174 | + * @param array $credential_source Stored WebAuthn credential source. | |
| 175 | + * | |
| 176 | + * @return array | |
| 177 | + */ | |
| 178 | + private function normalize_legacy_trust_path( array $credential_source ): array { | |
| 179 | + $legacy_type = $credential_source['trustPath']['type'] ?? null; | |
| 180 | + $legacy_map = array( | |
| 181 | + 'Webauthn\\TrustPath\\EmptyTrustPath' => EmptyTrustPath::class, | |
| 182 | + 'Webauthn\\TrustPath\\EcdaaKeyIdTrustPath' => EcdaaKeyIdTrustPath::class, | |
| 183 | + 'Webauthn\\TrustPath\\CertificateTrustPath' => CertificateTrustPath::class, | |
| 184 | + ); | |
| 185 | + | |
| 186 | + if ( is_string( $legacy_type ) && isset( $legacy_map[ $legacy_type ] ) ) { | |
| 187 | + $credential_source['trustPath']['type'] = $legacy_map[ $legacy_type ]; | |
| 188 | + } | |
| 189 | + | |
| 190 | + return $credential_source; | |
| 140 | 191 | } |
| 141 | 192 | |
| 142 | 193 | /** |
| 143 | 194 | * Store credential into database. |