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!")
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced
Remediation Steps
Locate the Finding
Review the data flow in the security scan report:
- Source: Where cryptographic operations occur (RSA encryption/decryption calls)
- Sink: The vulnerable padding method (
PKCS1_v1_5,PKCS1v15(), or absence of OAEP) - Library: Check if using deprecated PyCrypto vs. modern cryptography/PyCryptodome
- Note any frames between source and sink
Understand the Data Flow
Trace the cryptographic implementation:
- Identify the RSA library being used (
Crypto,cryptography) - Check the padding scheme (
PKCS1_v1_5,PKCS1v15(),OAEP) - Verify hash algorithm if OAEP is used (SHA-1 vs. SHA-256/SHA-384)
- Determine if encrypting sensitive data
- Check key size (should be ≥ 2048 bits)
Identify the Pattern
Match the code to vulnerable patterns:
- Using PyCrypto with
PKCS1_v1_5 - Using deprecated PyCrypto library
- Using
cryptographywithpadding.PKCS1v15() - Using OAEP with weak hash (SHA-1)
Apply the Fix
Choose the appropriate remediation approach based on your requirements:
Option 1: Migrate to cryptography library with OAEP (Recommended - most secure)
- Replace deprecated PyCrypto or PyCryptodome
PKCS1_v1_5withcryptographylibrary - Use
padding.OAEP()withMGF1and SHA-256 or stronger hash algorithms - Specify both main hash algorithm and MGF1 hash explicitly
- Set
label=Noneunless specific context binding is required
Option 2: Update PyCryptodome to use PKCS1_OAEP (If staying with PyCryptodome)
- Replace
PKCS1_v1_5cipher withPKCS1_OAEP - Always specify
hashAlgo=SHA256or stronger (never rely on defaults) - Ensure both encryption and decryption use same hash algorithm
- Consider migrating to
cryptographylibrary for better long-term support
Option 3: Upgrade weak hash algorithms (If already using OAEP)
- Replace SHA-1 with SHA-256, SHA-384, or SHA-512
- Update both main algorithm and MGF1 hash parameters
- Ensure hash algorithms match between encryption and decryption
- Use SHA-384 for higher security requirements or long-term confidentiality
Option 4: Implement hybrid encryption for large data (Data size > 190 bytes)
- RSA with OAEP has strict size limits based on key and hash algorithm
- Generate ephemeral AES-256 key for symmetric encryption
- Encrypt bulk data with AES-GCM, encrypt AES key with RSA-OAEP
- Combine encrypted key, nonce, and ciphertext for secure transmission
See the Secure Patterns and Remediation Strategy sections for detailed implementation examples of each approach.
Verify the Fix
Test and confirm:
- Test encryption/decryption works with OAEP
- Verify same plaintext produces different ciphertexts (probabilistic)
- Test with different message sizes (within RSA limits)
- Rescan with the security scanner to confirm finding is resolved
- Run unit tests to verify functionality
Check for Similar Issues
Search for related vulnerabilities:
- Search codebase for:
PKCS1_v1_5,PKCS1v15(),from Crypto.,PyCrypto - Review all RSA encryption/decryption operations
- Check for weak hash algorithms:
SHA1(),MD5() - Verify key sizes are ≥ 2048 bits
- Look for other cryptographic operations in the same module
Migration from PKCS#1 v1.5 to OAEP
Dual-Padding Decryption
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
def decrypt_with_fallback(private_key, ciphertext):
"""
Try OAEP first, fall back to PKCS#1 v1.5 for legacy data.
Use during migration period only.
"""
# Try secure OAEP first
try:
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return plaintext, 'OAEP'
except Exception:
pass # OAEP failed, try legacy
# Fall back to legacy PKCS#1 v1.5
try:
plaintext = private_key.decrypt(
ciphertext,
padding.PKCS1v15()
)
return plaintext, 'PKCS1v15'
except Exception as e:
raise ValueError(f"Decryption failed with both methods: {e}")
# Always encrypt with OAEP (never use PKCS1v15 for new data)
def encrypt_secure(public_key, plaintext):
return public_key.encrypt(
plaintext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)