GHSA-6HXQ-P678-4HR2
Vulnerability from github – Published: 2026-09-04 17:33 – Updated: 2026-09-04 17:33Summary
validateCertificatePath() does not verify that an attestation's certificate chain actually terminates at a configured trust anchor. When walking the chain it stops at the first self-signed certificate it finds (which could be user-supplied), and exits early.
This happens before the configured Apple/Google/etc trust anchor (which is concatenated to the end of the chain) is reached.
A user can therefore register a credential and have the server accept it as if it were backed by a genuine Apple / Android SafetyNet / Yubikey / etc.
Details
packages/server/src/helpers/validateCertificatePath.ts:
The configured trust anchor is appended to the end of the untrusted chain (line 83):
const x5cWithTrustAnchor = x5cCertsParsed.concat([anchor]);
The walk then verifies each cert was signed by the next, but breaks on the first self-signed cert (lines 104–116):
if (issuer.subject === issuer.issuer) {
// Root cert detected, make sure it signed itself
const issuerSignedIssuer = await issuer.verify(
{ publicKey: issuer.publicKey, signatureOnly: true },
WebCrypto,
);
if (!issuerSignedIssuer) {
throw new InvalidSubjectAndIssuer();
}
break; // <-- exits before the appended trust anchor is ever checked if user supplied self-signed cert comes first
}
The success condition is therefore "the certs form an internally-consistent chain ending in some self-signed cert" Rather than "the chain terminates at one of the configured trust anchors."
Exploit shape
attacker sends: x5c = [ forgedLeaf (signed by attacker root),
attackerSelfSignedRoot ]
library builds: [ forgedLeaf, attackerSelfSignedRoot, <configured Apple/Google/etc root> ]
walk: forgedLeaf -> attackerSelfSignedRoot (verifies, attacker controls both)
attackerSelfSignedRoot is self-signed -> break
attackerSelfSignedRoot -> configured root (NEVER CHECKED)
return true. The configured anchor never gets checked.
As far as observed, all attestation enforcement uses validateCertificatePath when using MDS etc.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 13.3.1"
},
"package": {
"ecosystem": "npm",
"name": "@simplewebauthn/server"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "13.3.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-295",
"CWE-296"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-04T17:33:20Z",
"nvd_published_at": null,
"severity": "LOW"
},
"details": "## Summary\n`validateCertificatePath()` does not verify that an attestation\u0027s certificate chain actually terminates at a configured trust anchor. When walking the chain it stops at the first self-signed certificate it finds (which could be user-supplied), and exits early.\n\nThis happens before the configured Apple/Google/etc trust anchor (which is concatenated to the end of the chain) is reached.\n\nA user can therefore register a credential and have the server accept it as if it were backed by a genuine Apple / Android SafetyNet / Yubikey / etc.\n\n## Details\n`packages/server/src/helpers/validateCertificatePath.ts`:\n\nThe configured trust anchor is appended to the end of the untrusted chain (line **83**):\n\n```ts\nconst x5cWithTrustAnchor = x5cCertsParsed.concat([anchor]);\n```\n\nThe walk then verifies each cert was signed by the next, but breaks on the first self-signed cert (lines **104\u2013116**):\n\n```ts\nif (issuer.subject === issuer.issuer) {\n // Root cert detected, make sure it signed itself\n const issuerSignedIssuer = await issuer.verify(\n { publicKey: issuer.publicKey, signatureOnly: true },\n WebCrypto,\n );\n if (!issuerSignedIssuer) {\n throw new InvalidSubjectAndIssuer();\n }\n break; // \u003c-- exits before the appended trust anchor is ever checked if user supplied self-signed cert comes first\n}\n```\n\nThe success condition is therefore \"the certs form an internally-consistent chain ending in some self-signed cert\" Rather than \"the chain terminates at one of the configured trust anchors.\"\n\n## Exploit shape\n\n```\nattacker sends: x5c = [ forgedLeaf (signed by attacker root),\n attackerSelfSignedRoot ]\n\nlibrary builds: [ forgedLeaf, attackerSelfSignedRoot, \u003cconfigured Apple/Google/etc root\u003e ]\n\nwalk: forgedLeaf -\u003e attackerSelfSignedRoot (verifies, attacker controls both)\n attackerSelfSignedRoot is self-signed -\u003e break\n attackerSelfSignedRoot -\u003e configured root (NEVER CHECKED)\n```\n\n`return true`. The configured anchor never gets checked.\n\n\nAs far as observed, all attestation enforcement uses validateCertificatePath when using MDS etc.",
"id": "GHSA-6hxq-p678-4hr2",
"modified": "2026-09-04T17:33:20Z",
"published": "2026-09-04T17:33:20Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/MasterKale/SimpleWebAuthn/security/advisories/GHSA-6hxq-p678-4hr2"
},
{
"type": "WEB",
"url": "https://github.com/MasterKale/SimpleWebAuthn/commit/67a41fed3dfd96cab1dcf414f6d1792a72e06e35"
},
{
"type": "WEB",
"url": "https://github.com/MasterKale/SimpleWebAuthn/commit/8a53d70f42bcbb7c744ef1b90e6db35bc4b26d06"
},
{
"type": "WEB",
"url": "https://github.com/MasterKale/SimpleWebAuthn/commit/dd0d73c716a528e6645efafbd43a972b64df71f9"
},
{
"type": "PACKAGE",
"url": "https://github.com/MasterKale/SimpleWebAuthn"
},
{
"type": "WEB",
"url": "https://github.com/MasterKale/SimpleWebAuthn/releases/tag/v13.3.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:A/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "SimpleWebAuthn: Registration verification does not sufficiently ensure that attestation certificates chain to a trust anchor"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.