Skip to content

CWE-780: Use of RSA Without OAEP - C# / .NET

Overview

RSA encryption in .NET can be performed with two distinct padding schemes: PKCS#1 v1.5 and OAEP (Optimal Asymmetric Encryption Padding). PKCS#1 v1.5 padding, used when RSACryptoServiceProvider.Encrypt(data, fOAEP: false) is called, is vulnerable to Bleichenbacher's adaptive chosen-ciphertext attack, which allows an attacker who can observe the server's response to decryption attempts (a "padding oracle") to decrypt RSA-encrypted messages without the private key.

OAEP padding adds randomization and a hash-based message structure that eliminates the padding oracle vulnerability. In .NET, OAEP is enabled by passing fOAEP: true to the legacy RSACryptoServiceProvider API or by using the modern RSA.Create() API with RSAEncryptionPadding.OaepSHA256.

Primary Defence: Replace RSACryptoServiceProvider.Encrypt(data, false) with RSA.Create() and rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256). For payloads larger than ~190 bytes (the maximum for RSA-OAEP with a 2048-bit key), use hybrid encryption: encrypt data with AES-GCM and encrypt only the AES key with RSA-OAEP.

Common Vulnerable Patterns

RSACryptoServiceProvider with PKCS#1 v1.5

using System.Security.Cryptography;

// VULNERABLE - fOAEP = false uses PKCS#1 v1.5 padding
public static byte[] EncryptVulnerable(byte[] data, RSACryptoServiceProvider rsa)
{
    return rsa.Encrypt(data, fOAEP: false); // PKCS#1 v1.5 - vulnerable to Bleichenbacher
}

public static byte[] DecryptVulnerable(byte[] ciphertext, RSACryptoServiceProvider rsa)
{
    return rsa.Decrypt(ciphertext, fOAEP: false); // Must match - also vulnerable
}

Why this is vulnerable:

  • PKCS#1 v1.5 includes a padding structure that the RSA decryption operation verifies. If the server returns different responses for valid vs. invalid padding (a timing difference, an error message difference, or different behavior), an attacker can perform Bleichenbacher's attack and decrypt ciphertexts with as few as a million queries.

OaepSHA1 (Deprecated Hash)

// VULNERABLE - SHA-1 is deprecated; use SHA-256 or stronger
var rsa = RSA.Create(2048);
byte[] ciphertext = rsa.Encrypt(plaintext, RSAEncryptionPadding.OaepSHA1);

Why this is vulnerable:

  • RSAEncryptionPadding.OaepSHA1 uses SHA-1 as the hash function in the OAEP mask generation. While this does not directly enable Bleichenbacher's attack, SHA-1 is cryptographically weak and its use is discouraged by NIST. Prefer OaepSHA256 or stronger.

Direct RSA Encryption of Large Payloads

// PROBLEMATIC - RSA can only encrypt up to key_size_bytes - 42 bytes (for OAEP SHA-256)
// For a 2048-bit key: max ~190 bytes. Larger data causes a CryptographicException.
var rsa = RSA.Create(2048);
byte[] bigPayload = Encoding.UTF8.GetBytes(largeStringOver190Chars);
byte[] ciphertext = rsa.Encrypt(bigPayload, RSAEncryptionPadding.OaepSHA256); // throws!

Why this is vulnerable/problematic:

  • RSA is not designed for bulk encryption. Attempting to encrypt large data directly throws a CryptographicException. Applications that encounter this often fall back to weaker padding or split the data unsafely. Hybrid encryption is the correct approach.

Secure Patterns

RSA.Create() with OAEP-SHA256

using System.Security.Cryptography;

// SECURE: modern API with explicit OAEP-SHA256 padding
public static byte[] Encrypt(byte[] plaintext, RSA publicKey)
{
    // Max plaintext: key_size_in_bytes - 66 bytes (for OaepSHA256)
    // For 2048-bit key: 256 - 66 = 190 bytes max
    return publicKey.Encrypt(plaintext, RSAEncryptionPadding.OaepSHA256);
}

public static byte[] Decrypt(byte[] ciphertext, RSA privateKey)
{
    return privateKey.Decrypt(ciphertext, RSAEncryptionPadding.OaepSHA256);
}

Why this works:

  • RSAEncryptionPadding.OaepSHA256 uses OAEP with SHA-256 as the hash and MGF1 mask generation function. OAEP's randomized padding ensures identical plaintexts produce different ciphertexts, and its structure prevents padding oracle attacks.

Hybrid Encryption for Large Payloads

using System.Security.Cryptography;

// SECURE: hybrid encryption - RSA-OAEP encrypts the AES key; AES-GCM encrypts the data
public static HybridCiphertext HybridEncrypt(byte[] plaintext, RSA publicKey)
{
    // Generate a fresh random AES-256 key for this message
    byte[] aesKey = RandomNumberGenerator.GetBytes(32); // 256-bit
    byte[] nonce  = RandomNumberGenerator.GetBytes(12); // 96-bit GCM nonce

    byte[] ciphertext = new byte[plaintext.Length];
    byte[] tag = new byte[16]; // 128-bit authentication tag

    using var aesGcm = new AesGcm(aesKey, tagSizeInBytes: 16);
    aesGcm.Encrypt(nonce, plaintext, ciphertext, tag);

    // Encrypt the AES key with RSA-OAEP (32 bytes is well within the 190-byte limit)
    byte[] encryptedKey = publicKey.Encrypt(aesKey, RSAEncryptionPadding.OaepSHA256);

    return new HybridCiphertext(encryptedKey, nonce, ciphertext, tag);
}

public static byte[] HybridDecrypt(HybridCiphertext envelope, RSA privateKey)
{
    byte[] aesKey = privateKey.Decrypt(envelope.EncryptedKey, RSAEncryptionPadding.OaepSHA256);
    byte[] plaintext = new byte[envelope.Ciphertext.Length];

    using var aesGcm = new AesGcm(aesKey, tagSizeInBytes: 16);
    aesGcm.Decrypt(envelope.Nonce, envelope.Ciphertext, envelope.Tag, plaintext);

    return plaintext;
}

public record HybridCiphertext(byte[] EncryptedKey, byte[] Nonce, byte[] Ciphertext, byte[] Tag);

Why this works:

  • RSA-OAEP encrypts only the 32-byte AES key (well within the 190-byte limit). AES-256-GCM then encrypts the actual data of any size. AES-GCM provides both confidentiality (encryption) and integrity (authentication tag), preventing ciphertext tampering.
  • A fresh random AES key is generated per message, so even if one message's key is compromised, others remain secure.

Migrating from RSACryptoServiceProvider

// BEFORE (vulnerable):
using var rsaLegacy = new RSACryptoServiceProvider(2048);
byte[] ciphertext = rsaLegacy.Encrypt(plaintext, fOAEP: false); // PKCS#1 v1.5

// AFTER (secure):
using var rsa = RSA.Create(2048);
byte[] ciphertext = rsa.Encrypt(plaintext, RSAEncryptionPadding.OaepSHA256); // OAEP

// Import existing key material (if needed):
using var rsaNew = RSA.Create();
rsaNew.ImportParameters(rsaLegacy.ExportParameters(includePrivateParameters: true));
byte[] ciphertext = rsaNew.Encrypt(plaintext, RSAEncryptionPadding.OaepSHA256);

Remediation Steps

Locate the Finding

  • Source: Any RSA encryption operation
  • Sink: RSACryptoServiceProvider.Encrypt(data, false), RSACryptoServiceProvider.Decrypt(data, false), RSAEncryptionPadding.OaepSHA1

Apply the Fix

  • PRIORITY 1: Change rsa.Encrypt(data, false) to rsa.Encrypt(data, true) as a minimum fix
  • PRIORITY 2: Migrate to RSA.Create() with RSAEncryptionPadding.OaepSHA256 for all new and updated code
  • PRIORITY 3: For payloads > 190 bytes, implement hybrid encryption using AES-GCM + RSA-OAEP

Verify the Fix

  • Confirm roundtrip encrypt/decrypt still works with the new padding parameter
  • Attempt to decrypt a ciphertext encrypted with OaepSHA256 using PKCS#1 v1.5 padding - it should fail
  • Rescan with the security scanner to confirm the finding is resolved

Check for Similar Issues

Search for: fOAEP: false, fOAEP = false, RSACryptoServiceProvider.Encrypt, OaepSHA1

Testing

  • Normal input: encrypt and decrypt representative payloads using the new OAEP-SHA256 or hybrid encryption path.
  • Boundary input: test payloads near the RSA size limit and larger payloads that must use the hybrid AES-GCM pattern.
  • Malicious input: submit malformed, truncated, and legacy PKCS#1 v1.5 ciphertexts; confirm failures are handled uniformly without revealing padding details.

Additional Resources