VYPR
Medium severity5.3NVD Advisory· Published Jul 15, 2024· Updated Apr 15, 2026

CVE-2024-39912

CVE-2024-39912

Description

web-auth/webauthn-lib is an open source set of PHP libraries and a Symfony bundle to allow developers to integrate that authentication mechanism into their web applications. The ProfileBasedRequestOptionsBuilder method returns allowedCredentials without any credentials if no username was found. When WebAuthn is used as the first or only authentication method, an attacker can enumerate usernames based on the absence of the allowedCredentials property in the assertion options response. This allows enumeration of valid or invalid usernames. By knowing which usernames are valid, attackers can focus their efforts on a smaller set of potential targets, increasing the efficiency and likelihood of successful attacks. This issue has been addressed in version 4.9.0 and all users are advised to upgrade. There are no known workarounds for this vulnerability.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
web-auth/webauthn-libPackagist
>= 4.5.0, < 4.9.04.9.0
web-auth/webauthn-frameworkPackagist
>= 4.5.0, < 4.9.04.9.0

Patches

3
64de11f6cddc

Add feature to hide existing credentials

https://github.com/web-auth/webauthn-frameworkFlorent MorselliJul 6, 2024via ghsa
11 files changed · +52 10
  • phpstan-baseline.neon+10 5 modified
    @@ -692,6 +692,11 @@ parameters:
     			count: 1
     			path: src/symfony/src/Controller/AttestationControllerFactory.php
     
    +		-
    +			message: "#^Method Webauthn\\\\Bundle\\\\CredentialOptionsBuilder\\\\PublicKeyCredentialCreationOptionsBuilder\\:\\:getFromRequest\\(\\) invoked with 3 parameters, 2 required\\.$#"
    +			count: 1
    +			path: src/symfony/src/Controller/AttestationRequestController.php
    +
     		-
     			message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpFoundation\\\\Request\\:\\:getContentType\\(\\)\\.$#"
     			count: 1
    @@ -807,11 +812,6 @@ parameters:
     			count: 1
     			path: src/symfony/src/CredentialOptionsBuilder/ProfileBasedRequestOptionsBuilder.php
     
    -		-
    -			message: "#^Should not use function \"dump\", please change the code\\.$#"
    -			count: 1
    -			path: src/symfony/src/CredentialOptionsBuilder/ProfileBasedRequestOptionsBuilder.php
    -
     		-
     			message: """
     				#^Fetching class constant class of deprecated class Webauthn\\\\Bundle\\\\Event\\\\AuthenticatorAssertionResponseValidationFailedEvent\\:
    @@ -1061,6 +1061,11 @@ parameters:
     			count: 4
     			path: src/symfony/src/DependencyInjection/WebauthnExtension.php
     
    +		-
    +			message: "#^Cannot access offset 'hide_existing…' on mixed\\.$#"
    +			count: 1
    +			path: src/symfony/src/DependencyInjection/WebauthnExtension.php
    +
     		-
     			message: "#^Cannot access offset 'host' on mixed\\.$#"
     			count: 4
    
  • src/symfony/src/Controller/AttestationControllerFactory.php+3 1 modified
    @@ -72,13 +72,15 @@ public function createRequestController(
             OptionsStorage $optionStorage,
             CreationOptionsHandler $creationOptionsHandler,
             FailureHandler|AuthenticationFailureHandlerInterface $failureHandler,
    +        bool $hideExistingExcludedCredentials = false
         ): AttestationRequestController {
             return new AttestationRequestController(
                 $optionsBuilder,
                 $userEntityGuesser,
                 $optionStorage,
                 $creationOptionsHandler,
    -            $failureHandler
    +            $failureHandler,
    +            $hideExistingExcludedCredentials
             );
         }
     
    
  • src/symfony/src/Controller/AttestationRequestController.php+6 1 modified
    @@ -24,14 +24,19 @@ public function __construct(
             private readonly OptionsStorage $optionsStorage,
             private readonly CreationOptionsHandler $creationOptionsHandler,
             private readonly FailureHandler|AuthenticationFailureHandlerInterface $failureHandler,
    +        private readonly bool $hideExistingExcludedCredentials = false,
         ) {
         }
     
         public function __invoke(Request $request): Response
         {
             try {
                 $userEntity = $this->userEntityGuesser->findUserEntity($request);
    -            $publicKeyCredentialCreationOptions = $this->extractor->getFromRequest($request, $userEntity);
    +            $publicKeyCredentialCreationOptions = $this->extractor->getFromRequest(
    +                $request,
    +                $userEntity,
    +                $this->hideExistingExcludedCredentials
    +            );
     
                 $response = $this->creationOptionsHandler->onCreationOptions(
                     $publicKeyCredentialCreationOptions,
    
  • src/symfony/src/CredentialOptionsBuilder/ProfileBasedCreationOptionsBuilder.php+3 2 modified
    @@ -48,7 +48,8 @@ public function __construct(
     
         public function getFromRequest(
             Request $request,
    -        PublicKeyCredentialUserEntity $userEntity
    +        PublicKeyCredentialUserEntity $userEntity,
    +        bool $hideExistingExcludedCredentials = false
         ): PublicKeyCredentialCreationOptions {
             $format = method_exists(
                 $request,
    @@ -57,7 +58,7 @@ public function getFromRequest(
             $format === 'json' || throw new BadRequestHttpException('Only JSON content type allowed');
             $content = $request->getContent();
     
    -        $excludedCredentials = $this->getCredentials($userEntity);
    +        $excludedCredentials = $hideExistingExcludedCredentials === true ? [] : $this->getCredentials($userEntity);
             $optionsRequest = $this->getServerPublicKeyCredentialCreationOptionsRequest($content);
             $authenticatorSelectionData = $optionsRequest->authenticatorSelection;
             $authenticatorSelection = null;
    
  • src/symfony/src/CredentialOptionsBuilder/PublicKeyCredentialCreationOptionsBuilder.php+2 1 modified
    @@ -12,6 +12,7 @@ interface PublicKeyCredentialCreationOptionsBuilder
     {
         public function getFromRequest(
             Request $request,
    -        PublicKeyCredentialUserEntity $userEntity
    +        PublicKeyCredentialUserEntity $userEntity,
    +        /*bool $hideExistingExcludedCredentials = false*/
         ): PublicKeyCredentialCreationOptions;
     }
    
  • src/symfony/src/DependencyInjection/Configuration.php+6 0 modified
    @@ -354,6 +354,12 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
                 ->scalarNode('user_entity_guesser')
                 ->isRequired()
                 ->end()
    +            ->scalarNode('hide_existing_credentials')
    +                ->info(
    +                    'In order to prevent username enumeration, the existing credentials can be hidden. This is highly recommended when the attestation ceremony is performed by anonymous users.'
    +                )
    +                ->defaultFalse()
    +            ->end()
                 ->scalarNode('options_storage')
                 ->defaultValue(SessionStorage::class)
                 ->info('Service responsible of the options/user entity storage during the ceremony')
    
  • src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php+1 0 modified
    @@ -415,6 +415,7 @@ private function createAttestationRequestControllerAndRoute(
                     new Reference($optionsStorageId),
                     new Reference($optionsHandlerId),
                     new Reference($failureHandlerId),
    +                true,
                 ]);
             $this->createControllerAndRoute(
                 $container,
    
  • src/symfony/src/DependencyInjection/WebauthnExtension.php+1 0 modified
    @@ -215,6 +215,7 @@ private function loadCreationControllersSupport(ContainerBuilder $container, arr
                         new Reference($creationConfig['options_storage']),
                         new Reference($creationConfig['options_handler']),
                         new Reference($creationConfig['failure_handler']),
    +                    $creationConfig['hide_existing_credentials'] ?? false,
                     ])
                     ->addTag(DynamicRouteCompilerPass::TAG, [
                         'method' => $creationConfig['options_method'],
    
  • tests/symfony/config/config.yml+1 0 modified
    @@ -131,6 +131,7 @@ webauthn:
         enabled: true
         creation:
           test:
    +        hide_existing_credentials: true
             options_path: '/devices/add/options'
             result_path: '/devices/add'
             #host: null
    
  • tests/symfony/functional/Attestation/AdditionalAuthenticatorTest.php+1 0 modified
    @@ -64,6 +64,7 @@ public function anExistingUserCanAskForOptionsUsingTheDedicatedController(): voi
                 static::assertArrayHasKey($expectedKey, $data);
             }
             static::assertSame('ok', $data['status']);
    +        static::assertArrayNotHasKey('excludeCredentials', $data); // username enumeration prevention is enabled
         }
     
         #[Test]
    
  • tests/symfony/functional/PublicKeyCredentialSourceRepository.php+18 0 modified
    @@ -38,6 +38,24 @@ public function __construct(
                 100
             );
             $this->saveCredentialSource($publicKeyCredentialSource1);
    +        $publicKeyCredentialSource2 = PublicKeyCredentialSource::create(
    +            base64_decode(
    +                'Ac8zKrpVWv9UCwxY1FyMqkESz2lV4CNwTk2+Hp19LgKbvh5uQ2/i6AMbTbTz1zcNapCEeiLJPlAAVM4L7AIow6I=',
    +                true
    +            ),
    +            PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
    +            [],
    +            AttestationStatement::TYPE_NONE,
    +            EmptyTrustPath::create(),
    +            Uuid::fromBinary(base64_decode('AAAAAAAAAAAAAAAAAAAAAA==', true)),
    +            base64_decode(
    +                'pQECAyYgASFYIJV56vRrFusoDf9hm3iDmllcxxXzzKyO9WruKw4kWx7zIlgg/nq63l8IMJcIdKDJcXRh9hoz0L+nVwP1Oxil3/oNQYs=',
    +                true
    +            ),
    +            '929fba2f-2361-4bc6-a917-bb76aa14c7f9',
    +            100
    +        );
    +        $this->saveCredentialSource($publicKeyCredentialSource2);
         }
     
         public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource
    
a9d1352897fb

Merge pull request #617 from web-auth/bugs/username-enumeration

https://github.com/web-auth/webauthn-frameworkFlorent MorselliJul 6, 2024via ghsa
11 files changed · +52 10
  • phpstan-baseline.neon+10 5 modified
    @@ -692,6 +692,11 @@ parameters:
     			count: 1
     			path: src/symfony/src/Controller/AttestationControllerFactory.php
     
    +		-
    +			message: "#^Method Webauthn\\\\Bundle\\\\CredentialOptionsBuilder\\\\PublicKeyCredentialCreationOptionsBuilder\\:\\:getFromRequest\\(\\) invoked with 3 parameters, 2 required\\.$#"
    +			count: 1
    +			path: src/symfony/src/Controller/AttestationRequestController.php
    +
     		-
     			message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpFoundation\\\\Request\\:\\:getContentType\\(\\)\\.$#"
     			count: 1
    @@ -807,11 +812,6 @@ parameters:
     			count: 1
     			path: src/symfony/src/CredentialOptionsBuilder/ProfileBasedRequestOptionsBuilder.php
     
    -		-
    -			message: "#^Should not use function \"dump\", please change the code\\.$#"
    -			count: 1
    -			path: src/symfony/src/CredentialOptionsBuilder/ProfileBasedRequestOptionsBuilder.php
    -
     		-
     			message: """
     				#^Fetching class constant class of deprecated class Webauthn\\\\Bundle\\\\Event\\\\AuthenticatorAssertionResponseValidationFailedEvent\\:
    @@ -1061,6 +1061,11 @@ parameters:
     			count: 4
     			path: src/symfony/src/DependencyInjection/WebauthnExtension.php
     
    +		-
    +			message: "#^Cannot access offset 'hide_existing…' on mixed\\.$#"
    +			count: 1
    +			path: src/symfony/src/DependencyInjection/WebauthnExtension.php
    +
     		-
     			message: "#^Cannot access offset 'host' on mixed\\.$#"
     			count: 4
    
  • src/symfony/src/Controller/AttestationControllerFactory.php+3 1 modified
    @@ -72,13 +72,15 @@ public function createRequestController(
             OptionsStorage $optionStorage,
             CreationOptionsHandler $creationOptionsHandler,
             FailureHandler|AuthenticationFailureHandlerInterface $failureHandler,
    +        bool $hideExistingExcludedCredentials = false
         ): AttestationRequestController {
             return new AttestationRequestController(
                 $optionsBuilder,
                 $userEntityGuesser,
                 $optionStorage,
                 $creationOptionsHandler,
    -            $failureHandler
    +            $failureHandler,
    +            $hideExistingExcludedCredentials
             );
         }
     
    
  • src/symfony/src/Controller/AttestationRequestController.php+6 1 modified
    @@ -24,14 +24,19 @@ public function __construct(
             private readonly OptionsStorage $optionsStorage,
             private readonly CreationOptionsHandler $creationOptionsHandler,
             private readonly FailureHandler|AuthenticationFailureHandlerInterface $failureHandler,
    +        private readonly bool $hideExistingExcludedCredentials = false,
         ) {
         }
     
         public function __invoke(Request $request): Response
         {
             try {
                 $userEntity = $this->userEntityGuesser->findUserEntity($request);
    -            $publicKeyCredentialCreationOptions = $this->extractor->getFromRequest($request, $userEntity);
    +            $publicKeyCredentialCreationOptions = $this->extractor->getFromRequest(
    +                $request,
    +                $userEntity,
    +                $this->hideExistingExcludedCredentials
    +            );
     
                 $response = $this->creationOptionsHandler->onCreationOptions(
                     $publicKeyCredentialCreationOptions,
    
  • src/symfony/src/CredentialOptionsBuilder/ProfileBasedCreationOptionsBuilder.php+3 2 modified
    @@ -48,7 +48,8 @@ public function __construct(
     
         public function getFromRequest(
             Request $request,
    -        PublicKeyCredentialUserEntity $userEntity
    +        PublicKeyCredentialUserEntity $userEntity,
    +        bool $hideExistingExcludedCredentials = false
         ): PublicKeyCredentialCreationOptions {
             $format = method_exists(
                 $request,
    @@ -57,7 +58,7 @@ public function getFromRequest(
             $format === 'json' || throw new BadRequestHttpException('Only JSON content type allowed');
             $content = $request->getContent();
     
    -        $excludedCredentials = $this->getCredentials($userEntity);
    +        $excludedCredentials = $hideExistingExcludedCredentials === true ? [] : $this->getCredentials($userEntity);
             $optionsRequest = $this->getServerPublicKeyCredentialCreationOptionsRequest($content);
             $authenticatorSelectionData = $optionsRequest->authenticatorSelection;
             $authenticatorSelection = null;
    
  • src/symfony/src/CredentialOptionsBuilder/PublicKeyCredentialCreationOptionsBuilder.php+2 1 modified
    @@ -12,6 +12,7 @@ interface PublicKeyCredentialCreationOptionsBuilder
     {
         public function getFromRequest(
             Request $request,
    -        PublicKeyCredentialUserEntity $userEntity
    +        PublicKeyCredentialUserEntity $userEntity,
    +        /*bool $hideExistingExcludedCredentials = false*/
         ): PublicKeyCredentialCreationOptions;
     }
    
  • src/symfony/src/DependencyInjection/Configuration.php+6 0 modified
    @@ -354,6 +354,12 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
                 ->scalarNode('user_entity_guesser')
                 ->isRequired()
                 ->end()
    +            ->scalarNode('hide_existing_credentials')
    +                ->info(
    +                    'In order to prevent username enumeration, the existing credentials can be hidden. This is highly recommended when the attestation ceremony is performed by anonymous users.'
    +                )
    +                ->defaultFalse()
    +            ->end()
                 ->scalarNode('options_storage')
                 ->defaultValue(SessionStorage::class)
                 ->info('Service responsible of the options/user entity storage during the ceremony')
    
  • src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php+1 0 modified
    @@ -415,6 +415,7 @@ private function createAttestationRequestControllerAndRoute(
                     new Reference($optionsStorageId),
                     new Reference($optionsHandlerId),
                     new Reference($failureHandlerId),
    +                true,
                 ]);
             $this->createControllerAndRoute(
                 $container,
    
  • src/symfony/src/DependencyInjection/WebauthnExtension.php+1 0 modified
    @@ -215,6 +215,7 @@ private function loadCreationControllersSupport(ContainerBuilder $container, arr
                         new Reference($creationConfig['options_storage']),
                         new Reference($creationConfig['options_handler']),
                         new Reference($creationConfig['failure_handler']),
    +                    $creationConfig['hide_existing_credentials'] ?? false,
                     ])
                     ->addTag(DynamicRouteCompilerPass::TAG, [
                         'method' => $creationConfig['options_method'],
    
  • tests/symfony/config/config.yml+1 0 modified
    @@ -131,6 +131,7 @@ webauthn:
         enabled: true
         creation:
           test:
    +        hide_existing_credentials: true
             options_path: '/devices/add/options'
             result_path: '/devices/add'
             #host: null
    
  • tests/symfony/functional/Attestation/AdditionalAuthenticatorTest.php+1 0 modified
    @@ -64,6 +64,7 @@ public function anExistingUserCanAskForOptionsUsingTheDedicatedController(): voi
                 static::assertArrayHasKey($expectedKey, $data);
             }
             static::assertSame('ok', $data['status']);
    +        static::assertArrayNotHasKey('excludeCredentials', $data); // username enumeration prevention is enabled
         }
     
         #[Test]
    
  • tests/symfony/functional/PublicKeyCredentialSourceRepository.php+18 0 modified
    @@ -38,6 +38,24 @@ public function __construct(
                 100
             );
             $this->saveCredentialSource($publicKeyCredentialSource1);
    +        $publicKeyCredentialSource2 = PublicKeyCredentialSource::create(
    +            base64_decode(
    +                'Ac8zKrpVWv9UCwxY1FyMqkESz2lV4CNwTk2+Hp19LgKbvh5uQ2/i6AMbTbTz1zcNapCEeiLJPlAAVM4L7AIow6I=',
    +                true
    +            ),
    +            PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
    +            [],
    +            AttestationStatement::TYPE_NONE,
    +            EmptyTrustPath::create(),
    +            Uuid::fromBinary(base64_decode('AAAAAAAAAAAAAAAAAAAAAA==', true)),
    +            base64_decode(
    +                'pQECAyYgASFYIJV56vRrFusoDf9hm3iDmllcxxXzzKyO9WruKw4kWx7zIlgg/nq63l8IMJcIdKDJcXRh9hoz0L+nVwP1Oxil3/oNQYs=',
    +                true
    +            ),
    +            '929fba2f-2361-4bc6-a917-bb76aa14c7f9',
    +            100
    +        );
    +        $this->saveCredentialSource($publicKeyCredentialSource2);
         }
     
         public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource
    
b6798de27cde

Add FakeCredentialGenerator for preventing username enumeration (#603)

https://github.com/web-auth/webauthn-libFlorent MorselliJun 29, 2024via ghsa
2 files changed · +82 0
  • src/FakeCredentialGenerator.php+15 0 added
    @@ -0,0 +1,15 @@
    +<?php
    +
    +declare(strict_types=1);
    +
    +namespace Webauthn;
    +
    +use Symfony\Component\HttpFoundation\Request;
    +
    +interface FakeCredentialGenerator
    +{
    +    /**
    +     * @return PublicKeyCredentialDescriptor[]
    +     */
    +    public function generate(Request $request, string $username): array;
    +}
    
  • src/SimpleFakeCredentialGenerator.php+67 0 added
    @@ -0,0 +1,67 @@
    +<?php
    +
    +declare(strict_types=1);
    +
    +namespace Webauthn;
    +
    +use Psr\Cache\CacheItemPoolInterface;
    +use Symfony\Component\HttpFoundation\Request;
    +use function count;
    +use function is_int;
    +
    +final class SimpleFakeCredentialGenerator implements FakeCredentialGenerator
    +{
    +    public function __construct(
    +        private readonly null|CacheItemPoolInterface $cache = null
    +    ) {
    +    }
    +
    +    /**
    +     * @return PublicKeyCredentialDescriptor[]
    +     */
    +    public function generate(Request $request, string $username): array
    +    {
    +        if ($this->cache === null) {
    +            return $this->generateCredentials($username);
    +        }
    +
    +        $cacheKey = 'fake_credentials_' . hash('xxh128', $username);
    +        $cacheItem = $this->cache->getItem($cacheKey);
    +        if ($cacheItem->isHit()) {
    +            return $cacheItem->get();
    +        }
    +
    +        $credentials = $this->generateCredentials($username);
    +        $cacheItem->set($credentials);
    +        $this->cache->save($cacheItem);
    +
    +        return $credentials;
    +    }
    +
    +    /**
    +     * @return PublicKeyCredentialDescriptor[]
    +     */
    +    private function generateCredentials(string $username): array
    +    {
    +        $transports = [
    +            PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_USB,
    +            PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_NFC,
    +            PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_BLE,
    +        ];
    +        $credentials = [];
    +        for ($i = 0; $i < random_int(1, 3); $i++) {
    +            $randomTransportKeys = array_rand($transports, random_int(1, count($transports)));
    +            if (is_int($randomTransportKeys)) {
    +                $randomTransportKeys = [$randomTransportKeys];
    +            }
    +            $randomTransports = array_values(array_intersect_key($transports, array_flip($randomTransportKeys)));
    +            $credentials[] = PublicKeyCredentialDescriptor::create(
    +                PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
    +                hash('sha256', random_bytes(16) . $username),
    +                $randomTransports
    +            );
    +        }
    +
    +        return $credentials;
    +    }
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.