Closed Bug 2023209 (CVE-2026-6767) Opened 6 months ago Closed 6 months ago

[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)

RESOLVED FIXED
Tracking Status
nss + 3.122
firefox-esr115 150+ fixed
firefox-esr140 150+ 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):

  1. 13 > 16 → FALSE → Path 1 (subdomain handling) is skipped
  2. Path 2 activates: presented.Peek('*') → TRUE
  3. Skip * from presented → presented reader now at .example.com (12 bytes remaining)
  4. Read from reference until .: reads m, a, i, l → stops at . → reference now at .example.com (12 bytes remaining)
  5. 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; b read 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:

  1. Root CA issues intermediate CA certificate with:
    • nameConstraints.permittedSubtrees = [dNSName: "mail.example.com"]
  2. 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)
  3. NSS validates the chain during a TLS handshake:
    • mozpkix checks leaf SAN *.example.com against intermediate's permitted constraint mail.example.com
    • Due to the bug, matches = true → constraint passes
  4. The chain is accepted for any hostname matching *.example.com (e.g., www.example.com, evil.example.com)
  5. 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);
Flags: sec-bounty?
Assignee: nobody → nobody
Group: firefox-core-security → crypto-core-security
Component: Security → Libraries
Product: Firefox → NSS

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: nobody → dkeeler
Severity: -- → S2
Keywords: sec-moderate
Priority: -- → P1
Status: UNCONFIRMED → NEW
Ever confirmed: true
Attached file (secure) —

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

Status: NEW → RESOLVED
Closed: 6 months ago
Resolution: --- → FIXED
Group: crypto-core-security → core-security-release
Flags: in-testsuite+
Flags: sec-bounty? → sec-bounty+
QA Whiteboard: [sec] [qa-triage-done-c151/b150]
Blocks: 2031033
status-nss: --- → 3.122
tracking-nss: --- → +
Blocks: 2031176
Pushed by jschanck@mozilla.com: https://hg.mozilla.org/projects/nss/rev/4e693e8b5c0d ensure permittedSubtrees don't match wildcards that could be outside the permitted tree r?jschanck
Pushed by jschanck@mozilla.com: https://hg.mozilla.org/projects/nss/rev/e7032c728910 ensure permittedSubtrees don't match wildcards that could be outside the permitted tree r?jschanck
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main150+]
Whiteboard: [client-bounty-form][adv-main150+] → [client-bounty-form][adv-main150+][adv-esr115.35+]
Whiteboard: [client-bounty-form][adv-main150+][adv-esr115.35+] → [client-bounty-form][adv-main150+][adv-esr115.35+][adv-esr140.10+]
Alias: CVE-2026-6767
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: