[CWE-295] NSS mozpkix Wildcard Bypasses Narrow DNS Name Constraint — CA Namespace Escape
Categories
(NSS :: Libraries, defect, P1)
Tracking
(nss+ 3.122, firefox-esr115150+ fixed, firefox-esr140150+ fixed, firefox148 wontfix, firefox149 wontfix, firefox150+ fixed)
People
(Reporter: harutokimura0608, Assigned: keeler)
References
()
Details
(Keywords: csectype-other, reporter-external, sec-moderate, Whiteboard: [client-bounty-form][adv-main150+][adv-esr115.35+][adv-esr140.10+])
Attachments
(2 files)
CWE: CWE-295 – Improper Certificate Validation
Affected Products
- NSS: All versions with mozpkix — tested on latest (commit at time of testing)
- Firefox: Directly affected — mozpkix is Firefox's default certificate verification engine;
mozilla::pkix::BuildCertChain()is called during every TLS handshake - Thunderbird: Directly affected — uses mozpkix for TLS certificate chain validation
- Other: Any application calling
mozilla::pkix::BuildCertChain()directly (mozpkix C++ API) - Note:
CERT_PKIXVerifyCert()(the public NSS C API) routes through the separate libpkix engine, which correctly rejects this chain. Only the mozpkix engine is affected. - Platform tested: Ubuntu 22.04 x86_64, Clang (no ASAN required — logic bug, not memory corruption)
2. Severity and CVSS
Severity: Medium-High
CVSS:3.1 Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:H/A:N
CVSS Score: 6.8 (Medium)
| Metric | Value | Rationale |
|---|---|---|
| Attack Vector | Network | Exploited during TLS handshake or certificate chain validation |
| Attack Complexity | High | Requires a mis-issued wildcard certificate from a name-constrained CA |
| Privileges Required | None | Attacker only needs the certificate — no session credentials |
| User Interaction | None | Certificate validation runs automatically |
| Scope | Changed | Bypasses trust constraints applied to a CA's issuance scope |
| Confidentiality | Low | May enable access to resources accessible via hostname match |
| Integrity | High | Certificate impersonation for hostnames outside the constrained namespace |
| Availability | None | — |
3. Environment
- OS: Ubuntu 22.04 x86_64
- Compiler: clang (default Ubuntu 22.04), no ASAN required — logic bug, not memory corruption
- Target: NSS latest (github.com/nss-dev/nss)
- Build:
CC=clang CCC=clang++ ./build.sh --clang -v - Evidence Type: cert-verification (incorrect acceptance of certificate chain that violates name constraints)
4. Affected Component
- Library: mozpkix
- File:
lib/mozpkix/lib/pkixnames.cpp - Lines: 887 (
CheckPresentedIDConformsToNameConstraintsSubtrees), 1182–1199 (MatchPresentedDNSIDWithReferenceDNSID) - Function:
MatchPresentedDNSIDWithReferenceDNSID()
5. Root Cause Analysis
Background
CheckPresentedIDConformsToNameConstraintsSubtrees() at line 887 calls MatchPresentedDNSIDWithReferenceDNSID() with AllowWildcards::Yes to check a presented DNS ID (from the certificate SAN) against a name constraint:
// pkixnames.cpp:887
case GeneralNameType::dNSName:
rv = MatchPresentedDNSIDWithReferenceDNSID(
presentedID, AllowWildcards::Yes, // ← wildcard allowed
AllowDotlessSubdomainMatches::Yes, IDRole::NameConstraint,
base, matches);
The Bug
Inside MatchPresentedDNSIDWithReferenceDNSID(), two code paths exist:
Path 1 — NameConstraint subdomain handling (lines 1116–1172): Activates only when presentedID.GetLength() > referenceDNSID.GetLength(). This correctly handles *.example.com (13 chars) vs broad constraint example.com (11 chars) — presented is longer, so subdomain logic runs.
Path 2 — Wildcard expansion (lines 1182–1199): Activates for ANY presented ID starting with *, regardless of IDRole:
// pkixnames.cpp:1182-1199
if (presented.Peek('*')) { // activates for *.example.com unconditionally
if (presented.Skip(1) != Success) { ... }
do {
if (reference.AtEnd()) { matches = false; return Success; }
uint8_t referenceByte;
if (reference.Read(referenceByte) != Success) { ... }
} while (!reference.Peek('.')); // skip until first '.' in reference
}
// Character-by-character comparison of remaining bytes
Bug scenario — *.example.com (13 chars) vs constraint mail.example.com (16 chars):
13 > 16→ FALSE → Path 1 (subdomain handling) is skipped- Path 2 activates:
presented.Peek('*')→ TRUE - Skip
*from presented → presented reader now at.example.com(12 bytes remaining) - Read from reference until
.: readsm,a,i,l→ stops at.→ reference now at.example.com(12 bytes remaining) - Character comparison:
.example.com==.example.com→matches = true
Expected behavior: *.example.com should not match the narrow constraint mail.example.com. A wildcard covering *.example.com permits hostnames like www.example.com, api.example.com — all outside the constrained namespace mail.example.com.
Actual behavior: mozpkix incorrectly returns matches = true, causing the permitted-subtrees check to pass.
Additional confirmed case
Equal-length: *.a.com (7 chars) vs constraint b.a.com (7 chars):
7 > 7→ FALSE → Path 1 skipped- Path 2:
*stripped →.a.com;bread from reference →.a.com - Comparison:
.a.com==.a.com→matches = true(should be false)
RFC Reference
RFC 5280 Section 4.2.1.10:
"For DNS names, the constraint applies to the host part of the name. [...] A DNS name constraint begins with a period (e.g.,
.example.com). The DNS name is included in the constraint if the host part of the DNS name ends with the constraint's host part."
RFC 6125 Section 6.4.3:
Wildcard matching is defined for hostname verification against presented IDs, not for name constraint enforcement.
The wildcard expansion in MatchPresentedDNSIDWithReferenceDNSID is semantically incorrect when referenceDNSIDRole == IDRole::NameConstraint. The subdomain-handling code at Path 1 already correctly handles wildcard certificates against broader constraints; the wildcard expansion at Path 2 should not run during constraint checking.
6. Attack Scenario / Exploitation Path
Prerequisite: An intermediate CA with a narrow permittedSubtrees constraint (e.g., dNSName: mail.example.com) issues a wildcard end-entity certificate (SAN: *.example.com).
Steps:
- Root CA issues intermediate CA certificate with:
nameConstraints.permittedSubtrees = [dNSName: "mail.example.com"]
- Intermediate CA issues leaf certificate with:
SAN: dNSName: *.example.com
(This should be prevented by the CA software, but if mis-issued or attacker controls the intermediate CA)
- NSS validates the chain during a TLS handshake:
- mozpkix checks leaf SAN
*.example.comagainst intermediate's permitted constraintmail.example.com - Due to the bug,
matches = true→ constraint passes
- mozpkix checks leaf SAN
- The chain is accepted for any hostname matching
*.example.com(e.g.,www.example.com,evil.example.com) - Firefox, curl with NSS, or any application using NSS/mozpkix accepts the certificate
Note on API paths: NSS contains two independent certificate verification engines.
CERT_PKIXVerifyCert() (the public C API) routes through libpkix (lib/libpkix/),
which correctly rejects this chain. Firefox and Thunderbird call mozilla::pkix::BuildCertChain()
directly, which routes through mozpkix (lib/mozpkix/) — the affected engine. The PoC uses
mozpkix's own unit test framework (mozpkix_gtest) to demonstrate the bug in the exact code path
exercised during real TLS handshakes in Firefox.
Impact: A CA that was restricted to issuing certificates for mail.example.com (and its subdomains) can effectively escape its namespace and issue certificates covering all *.example.com subdomains. This defeats the security guarantee of name-constrained intermediate CAs.
7. CIA Triad Impact
Confidentiality — Low: A name-constrained CA that escapes its namespace via a wildcard
certificate could enable the attacker to impersonate hostnames the CA was not authorized to
cover, potentially intercepting TLS connections. However, the attacker must already control
(or compromise) the constrained intermediate CA to issue the wildcard certificate.
Integrity — High: The core impact is certificate impersonation. A CA constrained to
mail.example.com could issue *.example.com, which mozpkix incorrectly accepts. This
enables man-in-the-middle attacks against any *.example.com hostname for Firefox and
Thunderbird users, defeating the trust boundary enforced by name constraints.
Availability — None: The bug causes incorrect acceptance (not a crash or denial of
service). Certificate validation completes normally with an incorrect positive result.
8. One-Click Reproduction Script
Evidence Type: cert-verification (gtest shows incorrect Success where ERROR_CERT_NOT_IN_NAME_SPACE is expected)
Evidence captured via mozpkix_gtest on Ubuntu 22.04 x86_64 GCP VM.
The mozpkix unit test suite directly exercises CheckNameConstraints() in
lib/mozpkix/lib/pkixnames.cpp, which is the code path used by Firefox
and Thunderbird for certificate chain validation.
#!/usr/bin/env bash
set -euo pipefail
# ============================================================
# NSS mozpkix Wildcard Name Constraint Bypass PoC
# Target: NSS latest (github.com/nss-dev/nss)
# Evidence type: cert-verification (gtest shows incorrect Success
# where ERROR_CERT_NOT_IN_NAME_SPACE is expected)
# Requires: bare Ubuntu 22.04 VM with nothing pre-installed
# ============================================================
WORKDIR="$(pwd)/nss_poc_021"
mkdir -p "$WORKDIR" && cd "$WORKDIR"
# Step 1: Install dependencies
sudo apt-get update -qq
sudo apt-get install -y build-essential git python3 python3-pip \
clang ninja-build zlib1g-dev libgtest-dev 2>&1 | tail -5
python3 -m pip install --quiet gyp-next
export PATH="$HOME/.local/bin:$PATH"
# Step 2: Clone NSPR and NSS
git clone --depth 1 https://github.com/nss-dev/nspr.git nspr
git clone https://github.com/nss-dev/nss.git nss
echo "[+] NSS commit: $(git -C nss rev-parse HEAD)"
# Step 3: Patch pkixnames_tests.cpp to add wildcard + narrow constraint test cases
# Insert 2 new test entries at the end of NAME_CONSTRAINT_PARAMS array
TESTFILE="nss/gtests/mozpkix_gtest/pkixnames_tests.cpp"
python3 << 'PYEOF'
import sys
testfile = "nss/gtests/mozpkix_gtest/pkixnames_tests.cpp"
with open(testfile, 'r') as f:
content = f.read()
new_tests = '''\
// PoC: Wildcard presented ID against narrow permitted name constraint
// Bug: pkixnames.cpp:1182 wildcard expansion runs unconditionally.
// *.example.com (13 chars) vs mail.example.com (16 chars):
// 13 > 16 = FALSE -> Path 1 (subdomain handling) skipped
// Path 2: presented.Peek('*') = TRUE -> wildcard expansion
// Strips '*' from presented -> .example.com
// Reads 'mail' from reference -> .example.com
// .example.com == .example.com -> matches=true (BUG)
{ ByteString(), DNSName("*.example.com"),
GeneralSubtree(DNSName("mail.example.com")),
Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE
},
// Equal-length variant: *.a.com (7) vs b.a.com (7)
// 7 > 7 = FALSE -> Path 1 skipped -> Path 2 wildcard expansion -> matches=true
{ ByteString(), DNSName("*.a.com"),
GeneralSubtree(DNSName("b.a.com")),
Result::ERROR_CERT_NOT_IN_NAME_SPACE, Result::ERROR_CERT_NOT_IN_NAME_SPACE
},
'''
# Insert before the closing }; of NAME_CONSTRAINT_PARAMS array.
# Anchor: "// Handle a certificate with no DirectoryName:" is the last comment
# in the array, followed by the last entry and then "};".
marker = "// Handle a certificate with no DirectoryName:"
idx = content.find(marker)
if idx == -1:
print("ERROR: Could not find anchor comment in test file", file=sys.stderr)
sys.exit(1)
# Find the closing "};" after the marker
end_marker = "};"
end_idx = content.find(end_marker, idx)
if end_idx == -1:
print("ERROR: Could not find closing }; after anchor", file=sys.stderr)
sys.exit(1)
# Insert the new tests before "};"
content = content[:end_idx] + new_tests + content[end_idx:]
with open(testfile, 'w') as f:
f.write(content)
print("[+] Patched pkixnames_tests.cpp: added 2 wildcard+narrow-constraint test cases")
PYEOF
# Step 4: Build NSS (with tests enabled)
cd nss
CC=clang CCC=clang++ ./build.sh --clang -v 2>&1 | tail -20 || true
# Verify mozpkix_gtest was built
DIST="$(pwd)/../dist"
ls "${DIST}/Debug/bin/mozpkix_gtest" || {
echo "FATAL: mozpkix_gtest not built"
exit 1
}
echo "[+] Built mozpkix_gtest"
cd ..
# Step 5: Create empty NSS database (mozpkix_gtest requires NSS_Initialize)
NSSDB="${WORKDIR}/nssdb"
mkdir -p "$NSSDB"
LD_LIBRARY_PATH="${DIST}/Debug/lib" \
"${DIST}/Debug/bin/certutil" -N -d "sql:${NSSDB}" --empty-password 2>&1
echo "[+] Created NSS database at ${NSSDB}"
# Step 6: Run mozpkix_gtest
echo ""
echo "============================================"
echo "Running mozpkix_gtest — full suite"
echo "Expected: 2 FAILED tests (our new test cases)"
echo " Test: *.example.com vs permitted mail.example.com"
echo " Test: *.a.com vs permitted b.a.com"
echo "============================================"
echo ""
LD_LIBRARY_PATH="${DIST}/Debug/lib" \
"${DIST}/Debug/bin/mozpkix_gtest" \
-d "${NSSDB}" \
--gtest_filter="pkixnames_CheckNameConstraints*" \
2>&1 || true
echo ""
echo "============================================"
echo "INTERPRETATION:"
echo " If tests NameConstraintsEnforcedForDirectlyIssuedEndEntity/N FAIL with:"
echo " expected: Result::ERROR_CERT_NOT_IN_NAME_SPACE"
echo " actual: Result::Success"
echo " Then the bug is CONFIRMED: mozpkix incorrectly matches wildcards"
echo " against narrow name constraints via pkixnames.cpp:1182"
echo "============================================"
Verification Evidence (cert-verification type — gtest output):
| Test Case | Expected (RFC 5280) | Actual (mozpkix) | Result |
|---|---|---|---|
*.example.com vs permitted mail.example.com |
ERROR_CERT_NOT_IN_NAME_SPACE |
Success |
FAIL — BUG |
*.a.com vs permitted b.a.com |
ERROR_CERT_NOT_IN_NAME_SPACE |
Success |
FAIL — BUG |
Note: CERT_PKIXVerifyCert() (the public NSS C API) uses the separate
libpkix engine (lib/libpkix/), which has its own name constraint
implementation that correctly rejects this chain. The bug is exclusively in
mozpkix (lib/mozpkix/), which Firefox and Thunderbird call directly via
mozilla::pkix::BuildCertChain() → SSL_AuthCertificate(). The gtest approach
directly exercises the buggy mozpkix code path.
9. Suggested Fix
Add a role guard at line 1182 in pkixnames.cpp to prevent wildcard expansion during name constraint checking:
// CURRENT (BUGGY):
if (presented.Peek('*')) {
// FIXED:
if (referenceDNSIDRole != IDRole::NameConstraint && presented.Peek('*')) {
Rationale: When the reference ID is a name constraint, *.example.com should be checked as a subdomain of the constraint's domain without wildcard expansion. The subdomain-handling code at Path 1 (lines 1116–1172) already correctly handles *.example.com against a broader constraint like example.com (where *.example.com length 13 > example.com length 11). The wildcard expansion at Path 2 is only semantically valid during hostname verification (IDRole::PresentedID) where the presented wildcard matches individual hostnames.
Test cases to add in pkixnames_tests.cpp:
// Should be ERROR_CERT_NOT_IN_NAME_SPACE:
CheckPresentedIDConformsToConstraints(
"*.example.com", "permitted;DNS:mail.example.com",
/*expectedResult=*/Result::ERROR_CERT_NOT_IN_NAME_SPACE);
// Should be ERROR_CERT_NOT_IN_NAME_SPACE:
CheckPresentedIDConformsToConstraints(
"*.a.com", "permitted;DNS:b.a.com",
/*expectedResult=*/Result::ERROR_CERT_NOT_IN_NAME_SPACE);
Updated•6 months ago
|
| Assignee | ||
Comment 1•6 months ago
|
||
Nice find!
The worst-case scenario I can think of here is that some organization, say Example Corp, gets a technically constrained intermediate that can only issue certificates for payroll.example.com. They give control of that intermediate (I'm not even sure if this is allowed) to a third-party to do their payroll, but now that organization can issue certificates that are valid for *.example.com, which exceeds the issuing authority they've been delegated. I don't know how common this is, but sec-moderate seems appropriate.
| Assignee | ||
Updated•6 months ago
|
| Assignee | ||
Comment 2•6 months ago
|
||
Pushed by jschanck@mozilla.com:
https://hg.mozilla.org/projects/nss/rev/47c4713a849f
ensure permittedSubtrees don't match wildcards that could be outside the permitted tree r=jschanck
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•4 months ago
|
Updated•1 month ago
|
Description
•