Skip to content

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_5 uses 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_5 only 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=None is 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_OAEP adds randomness, making RSA semantically secure.
  • Explicit hashAlgo=SHA256 avoids 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

pip 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 cryptography with padding.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_5 with cryptography library
  • Use padding.OAEP() with MGF1 and SHA-256 or stronger hash algorithms
  • Specify both main hash algorithm and MGF1 hash explicitly
  • Set label=None unless specific context binding is required

Option 2: Update PyCryptodome to use PKCS1_OAEP (If staying with PyCryptodome)

  • Replace PKCS1_v1_5 cipher with PKCS1_OAEP
  • Always specify hashAlgo=SHA256 or stronger (never rely on defaults)
  • Ensure both encryption and decryption use same hash algorithm
  • Consider migrating to cryptography library 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
        )
    )

Additional Resources