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.OaepSHA1uses 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. PreferOaepSHA256or 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.OaepSHA256uses 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)torsa.Encrypt(data, true)as a minimum fix - PRIORITY 2: Migrate to
RSA.Create()withRSAEncryptionPadding.OaepSHA256for 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
OaepSHA256using 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.