| @@ -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,15 +39,38 @@ | ||
| 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 | * |
| 44 | - * @return mixed | |
| 70 | + * @return array | |
| 45 | 71 | */ |
| 46 | - public function getCredentials( int $user_id ) { | |
| 72 | + public function getCredentials( int $user_id ): array { | |
| 47 | 73 | return $this->get_user_meta( $user_id, self::CREDENTIAL_OPTION_KEY ); |
| 48 | 74 | } |
| 49 | 75 | |
| 50 | 76 | /** |
| @@ -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 | |
| @@ -120,14 +147,21 @@ | ||
| 120 | 147 | $user_data = $this->getCredentials( $user_id ); |
| 121 | 148 | |
| 122 | 149 | if ( is_array( $user_data ) ) { |
| 123 | 150 | foreach ( $user_data as $data ) { |
| 124 | - if ( ! empty( $type ) && ! empty( $data['authenticator_type'] ) && $type !== $data['authenticator_type'] ) { | |
| 151 | + if ( | |
| 152 | + ! in_array( $type, array( null, '' ), true ) | |
| 153 | + && isset( $data['authenticator_type'] ) | |
| 154 | + && '' !== $data['authenticator_type'] | |
| 155 | + && $type !== $data['authenticator_type'] | |
| 156 | + ) { | |
| 125 | 157 | continue; |
| 126 | 158 | } |
| 127 | 159 | |
| 128 | 160 | if ( isset( $data['credential_source'] ) ) { |
| 129 | - $sources[] = PublicKeyCredentialSource::createFromArray( $data['credential_source'] ); | |
| 161 | + $sources[] = PublicKeyCredentialSource::createFromArray( | |
| 162 | + $this->normalize_legacy_trust_path( $data['credential_source'] ) | |
| 163 | + ); | |
| 130 | 164 | } |
| 131 | 165 | } |
| 132 | 166 | } |
| 133 | 167 | |
| @@ -134,8 +168,30 @@ | ||
| 134 | 168 | return $sources; |
| 135 | 169 | } |
| 136 | 170 | |
| 137 | 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; | |
| 191 | + } | |
| 192 | + | |
| 193 | + /** | |
| 138 | 194 | * Store credential into database. |
| 139 | 195 | * |
| 140 | 196 | * @param PublicKeyCredentialSource $public_key_credential_source The credential source to store. |
| 141 | 197 | * |
| @@ -199,13 +255,13 @@ | ||
| 199 | 255 | * @return void |
| 200 | 256 | * @since 3.4.0 |
| 201 | 257 | */ |
| 202 | 258 | public function addUserHandleMatchFailed( $user, $data ): void { |
| 203 | - if ( ! empty( $user->ID ) && ! empty( $data['rawId'] ) ) { | |
| 259 | + if ( isset( $user->ID ) && $user->ID > 0 && '' !== $data['rawId'] ) { | |
| 204 | 260 | $meta_val = $this->getUserHandleMatchFailed( $user->ID ); |
| 205 | 261 | $meta_val['show_notice'] = $meta_val['show_notice'] ?? true; |
| 206 | 262 | |
| 207 | - if ( empty( $meta_val['authenticators'] ) || ! in_array( $data['rawId'], $meta_val['authenticators'], true ) ) { | |
| 263 | + if ( ! isset( $meta_val['authenticators'] ) || array() === $meta_val['authenticators'] || ! in_array( $data['rawId'], $meta_val['authenticators'], true ) ) { | |
| 208 | 264 | $meta_val['authenticators'][] = $data['rawId']; |
| 209 | 265 | } |
| 210 | 266 | |
| 211 | 267 | $this->setUserHandleMatchFailed( $user->ID, $meta_val ); |
| @@ -221,12 +277,12 @@ | ||
| 221 | 277 | * @return void |
| 222 | 278 | * @since 3.4.0 |
| 223 | 279 | */ |
| 224 | 280 | public function removeUserHandleMatchFailed( int $user_id, string $auth_id ): void { |
| 225 | - if ( ! empty( $auth_id ) ) { | |
| 281 | + if ( '' !== $auth_id ) { | |
| 226 | 282 | $meta_val = $this->getUserHandleMatchFailed( $user_id ); |
| 227 | 283 | |
| 228 | - if ( ! empty( $meta_val['authenticators'] ) && is_array( $meta_val['authenticators'] ) ) { | |
| 284 | + if ( isset( $meta_val['authenticators'] ) && is_array( $meta_val['authenticators'] ) && array() !== $meta_val['authenticators'] ) { | |
| 229 | 285 | $pos = array_search( $auth_id, $meta_val['authenticators'], true ); |
| 230 | 286 | |
| 231 | 287 | if ( false !== $pos ) { |
| 232 | 288 | array_splice( $meta_val['authenticators'], $pos, 1 ); |