CWE-780: Use of RSA Without OAEP - Python
Overview
Using RSA encryption without OAEP (Optimal Asymmetric Encryption Padding) enables padding oracle attacks, chosen ciphertext attacks, and message malleability. In Python, this typically occurs when using the deprecated Crypto library (PyCrypto) or when not specifying padding parameters correctly with the cryptography library.
Primary Defence: Use the cryptography library with padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None) for RSA encryption/decryption, avoid deprecated PyCrypto and PKCS1_v1_5 padding entirely, always specify OAEP padding explicitly with SHA-256 or stronger hash functions, and consider using hybrid encryption (RSA for key exchange + Fernet or AES-GCM for data) to prevent padding oracle attacks.
Common Vulnerable Patterns
Using PKCS1_v1_5 Padding from PyCryptodome
# VULNERABLE - PKCS#1 v1.5 padding is deprecated and vulnerable
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from Crypto.Random import get_random_bytes
key = RSA.generate(2048)
cipher = PKCS1_v1_5.new(key)
message = b"Sensitive data"
ciphertext = cipher.encrypt(message)
# Attack example:
# Vulnerable to Bleichenbacher's padding oracle attack
# Attacker can decrypt ciphertext without private key by observing padding errors
Why this is vulnerable:
PKCS1_v1_5uses legacy PKCS#1 v1.5 padding, broken since Bleichenbacher (1998).- Padding validation differences create a decryption oracle (errors/timing/exceptions).
- Attackers can send many crafted ciphertexts and recover plaintext bit by bit.
- Real-world exploits exist across TLS, XML encryption, and PKCS#11 systems.
- PyCryptodome keeps
PKCS1_v1_5only for legacy compatibility and warns against it.
Using Legacy PyCrypto Library
# VULNERABLE - PyCrypto is deprecated and unmaintained since 2013
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
key = RSA.generate(2048)
cipher = PKCS1_v1_5.new(key.publickey())
encrypted = cipher.encrypt(b"Secret message")
# Why vulnerable:
# - Library hasn't been updated since 2013
# - Uses vulnerable PKCS#1 v1.5 padding by default
# - Contains known security vulnerabilities
Why this is vulnerable:
- PyCrypto is unmaintained since 2013 with no security patches.
- Known CVEs include memory corruption, timing issues, and weak RNGs.
- It uses PKCS#1 v1.5, enabling Bleichenbacher-style attacks.
- Attackers can fingerprint the dependency and apply public exploits.
- It lacks modern standards support and breaks on newer Python versions.
Using cryptography Library with PKCS1v15 Padding
# VULNERABLE - Explicitly using PKCS1v15 instead of OAEP
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
public_key = private_key.public_key()
message = b"Confidential data"
ciphertext = public_key.encrypt(
message,
padding.PKCS1v15() # Vulnerable padding!
)
# Why vulnerable:
# - PKCS#1 v1.5 padding enables timing attacks
# - Deterministic encryption (same plaintext → same ciphertext)
# - Vulnerable to Bleichenbacher's attack
Why this is vulnerable:
padding.PKCS1v15()reintroduces PKCS#1 v1.5 padding oracles.- Deterministic padding makes identical plaintexts produce identical ciphertexts.
- Padding vs decryption errors can leak an oracle via exceptions or timing.
- Web apps often surface these signals through error responses at scale.
Using Weak Hash Algorithm with OAEP
# VULNERABLE - Using deprecated SHA-1 hash with OAEP
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
public_key = private_key.public_key()
ciphertext = public_key.encrypt(
b"Message",
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()), # Weak!
algorithm=hashes.SHA1(), # Deprecated!
label=None
)
)
# Why vulnerable:
# - SHA-1 is cryptographically broken
# - Reduces the security level of OAEP
**Why this is vulnerable:**
- OAEP is fine, but SHA-1 is broken (practical collisions).
- OAEP relies on a strong hash; SHA-1 undermines that security proof.
- SHA-1 offers only ~80-bit collision security (below modern minimums).
- Standards prohibit SHA-1, risking audit failures and compliance issues.
Secure Patterns
Using cryptography Library with OAEP and SHA-256
# SECURE - Modern cryptography library with OAEP padding and SHA-256
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
# Generate RSA key pair
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048, # Minimum 2048 bits
backend=default_backend()
)
public_key = private_key.public_key()
message = b"Sensitive data to encrypt"
# Encrypt with OAEP padding and SHA-256
ciphertext = public_key.encrypt(
message,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# Decrypt with OAEP padding
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
assert plaintext == message
Why this works:
- OAEP per RFC 8017 makes encryption probabilistic, blocking pattern analysis.
MGF1(SHA-256)spreads randomness across the padded message.- SHA-256 provides collision resistance for the OAEP security proof.
label=Noneis the standard interoperable default.
Using PyCryptodome with OAEP (Alternative to cryptography)
# SECURE - PyCryptodome (modern replacement for PyCrypto) with OAEP
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP # Use OAEP, not PKCS1_v1_5
from Crypto.Hash import SHA256
# Generate RSA key pair
key = RSA.generate(2048)
# Create cipher with OAEP padding
cipher = PKCS1_OAEP.new(key, hashAlgo=SHA256)
message = b"Confidential message"
# Encrypt with OAEP
ciphertext = cipher.encrypt(message)
# Decrypt with OAEP
plaintext = cipher.decrypt(ciphertext)
assert plaintext == message
Why this works:
PKCS1_OAEPadds randomness, making RSA semantically secure.- Explicit
hashAlgo=SHA256avoids weak default hashes. - Key and hash are bound at construction to prevent misconfiguration.
- Maintained fork with security fixes; drop-in for legacy PyCrypto code.
Using OAEP with SHA-384 for Higher Security
# SECURE - OAEP with SHA-384 for enhanced security
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096, # Higher key size for long-term security
)
public_key = private_key.public_key()
message = b"Highly sensitive data"
# Encrypt with OAEP using SHA-384
ciphertext = public_key.encrypt(
message,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA384()),
algorithm=hashes.SHA384(),
label=None
)
)
# Decrypt
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA384()),
algorithm=hashes.SHA384(),
label=None
)
)
Why this works:
- SHA-384 provides a higher security margin than SHA-256.
- 4096-bit RSA + SHA-384 meets high-assurance requirements.
- Trade-off: larger OAEP overhead and slightly smaller plaintext size.
- Use for long-retention or high-value data; SHA-256 is fine for most apps.
Hybrid Encryption for Large Data
# SECURE - Hybrid encryption: RSA-OAEP for key, AES-GCM for data
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
import os
def hybrid_encrypt(public_key, plaintext):
"""Encrypt large data using hybrid encryption"""
# Generate random AES key
aes_key = AESGCM.generate_key(bit_length=256)
# Encrypt data with AES-GCM
aesgcm = AESGCM(aes_key)
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
# Encrypt AES key with RSA-OAEP
encrypted_key = public_key.encrypt(
aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return {
'encrypted_key': encrypted_key,
'nonce': nonce,
'ciphertext': ciphertext
}
# Encrypts data of any size securely
large_data = b"Very large confidential document..." * 1000
encrypted = hybrid_encrypt(public_key, large_data)
Why this works:
- RSA size limits make direct encryption impractical for large data.
- AES-GCM handles bulk encryption efficiently and adds integrity.
- RSA-OAEP wraps only the short AES key within RSA limits.
- Mirrors TLS-style hybrid design for scalable, secure encryption.
Remediation Strategy
PRIORITY 1: Use cryptography Library with OAEP (PRIMARY FIX)
Use the modern cryptography library (not the deprecated Crypto/PyCrypto) with explicit OAEP padding.
Install cryptography
RSA Encryption with OAEP
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend
# Generate RSA key pair
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048, # Minimum 2048 bits
backend=default_backend()
)
public_key = private_key.public_key()
# Message to encrypt (must be bytes)
message = b"Sensitive data to encrypt"
# SECURE - Encrypt with OAEP padding
ciphertext = public_key.encrypt(
message,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None # Optional label, usually None
)
)
# SECURE - Decrypt with OAEP padding
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
assert plaintext == message
PRIORITY 2: Hybrid Encryption for Large Data
RSA is limited to small data. For 2048-bit RSA with SHA-256, max plaintext is ~190 bytes. Use hybrid encryption for larger data.
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
import os
def hybrid_encrypt(public_key, plaintext):
"""
Encrypt large data using hybrid encryption:
- Generate random AES key
- Encrypt data with AES-GCM
- Encrypt AES key with RSA-OAEP
"""
# Generate random 256-bit AES key
aes_key = AESGCM.generate_key(bit_length=256)
# Encrypt data with AES-GCM
aesgcm = AESGCM(aes_key)
nonce = os.urandom(12) # 96-bit nonce for GCM
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
# Encrypt AES key with RSA-OAEP
encrypted_key = public_key.encrypt(
aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return {
'encrypted_key': encrypted_key,
'nonce': nonce,
'ciphertext': ciphertext
}
def hybrid_decrypt(private_key, encrypted_data):
"""Decrypt hybrid-encrypted data"""
# Decrypt AES key with RSA-OAEP
aes_key = private_key.decrypt(
encrypted_data['encrypted_key'],
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# Decrypt data with AES-GCM
aesgcm = AESGCM(aes_key)
plaintext = aesgcm.decrypt(
encrypted_data['nonce'],
encrypted_data['ciphertext'],
None
)
return plaintext
# Example usage
large_data = b"This can be megabytes of data" * 1000
encrypted = hybrid_encrypt(public_key, large_data)
decrypted = hybrid_decrypt(private_key, encrypted)
assert decrypted == large_data
PRIORITY 3: RSA Signatures with PSS (Not OAEP)
For signatures (not encryption), use PSS padding instead of OAEP.
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
message = b"Document to sign"
# SECURE - Sign with PSS padding
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH # Recommended
),
hashes.SHA256()
)
# Verify signature
try:
public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("Signature valid!")
except Exception:
print("Signature invalid!")