CWE-780: Use of RSA Algorithm without OAEP - C# / .NET
Overview
RSA encryption in .NET uses one of two 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, RSA.Create() with RSAEncryptionPadding.OaepSHA256 selects OAEP over SHA-256. Passing fOAEP: true to the legacy RSACryptoServiceProvider also selects OAEP, but always over SHA-1 - on .NET 10 its ciphertext decrypts under OaepSHA1 and no other digest - so it closes the padding oracle without getting the code off SHA-1.
Primary Defence: Replace RSACryptoServiceProvider.Encrypt(data, false) with RSA.Create() and rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256). For payloads larger than ~318 bytes (the maximum for RSA-OAEP with a 3072-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 or a difference in the error it returns), 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(3072);
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
// VULNERABLE - Direct RSA Encryption of Large Payloads
// PROBLEMATIC - RSA can only encrypt up to key_size_bytes - 66 bytes with OaepSHA256
// (2 * 32-byte digest + 2; the familiar 42 is the SHA-1 figure, 2 * 20 + 2).
// For a 3072-bit key: max 318 bytes. Larger data causes a CryptographicException.
var rsa = RSA.Create(3072);
byte[] bigPayload = Encoding.UTF8.GetBytes(largeStringOver318Chars);
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 3072-bit key: 384 - 66 = 318 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 318-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 318-byte limit). AES-256-GCM then encrypts the actual data of any size. AES-GCM provides both confidentiality (encryption) and integrity (authentication tag), so a tampered ciphertext fails to decrypt instead of returning altered plaintext.
- 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[] legacyCiphertext = rsaLegacy.Encrypt(plaintext, fOAEP: false); // PKCS#1 v1.5
// AFTER (secure): carry the existing key over, change only the padding.
// ImportParameters preserves the key material, so certificates and published
// public keys stay valid. Moving 2048 -> 3072 is a separate, later migration that
// does require new key material. NIST accepts 2048 (112-bit) only through 2030,
// so generate new keys at 3072; do not conflate that with this padding fix.
using var rsa = RSA.Create();
rsa.ImportParameters(rsaLegacy.ExportParameters(includePrivateParameters: true));
byte[] ciphertext = rsa.Encrypt(plaintext, RSAEncryptionPadding.OaepSHA256); // OAEP
// The old ciphertext is not readable with the new padding - keep this path
// alive until the backfill completes.
byte[] recoveredLegacy = rsa.Decrypt(legacyCiphertext, RSAEncryptionPadding.Pkcs1);
byte[] recovered = rsa.Decrypt(ciphertext, RSAEncryptionPadding.OaepSHA256);
Why this works: The vulnerability is the fOAEP: false argument, not the class. That boolean selects PKCS#1 v1.5 padding, which is deterministic in structure and leaks whether a decryption produced a well-formed block - the signal Bleichenbacher's adaptive chosen-ciphertext attack uses to recover plaintext one query at a time without ever learning the private key. RSAEncryptionPadding.OaepSHA256 adds randomised padding with an integrity check, so a tampered ciphertext fails indistinguishably and yields the attacker nothing to iterate on.
Moving to RSA.Create() matters for a second reason: it is the platform-agnostic factory, while RSACryptoServiceProvider is the Windows CSP-backed implementation. EncryptValue/DecryptValue on it are obsolete as SYSLIB0048 and throw NotSupportedException (verified on .NET 10); Encrypt(byte[], bool) and Decrypt(byte[], bool) still compile without a warning there, so the fOAEP: false call site is a finding the compiler will not raise for you. Code that stays on this class will not behave consistently off Windows.
The ImportParameters step is what makes this a migration rather than a re-key. The key material is unchanged - only the padding scheme changes - so existing public keys stay valid and certificates do not need reissuing. Ciphertext is the part that does not carry over: anything already encrypted with PKCS#1 v1.5 must still be decrypted with RSAEncryptionPadding.Pkcs1 until it has been re-encrypted, so keep the old decryption path alive until the backfill completes.
Testing
- Round trip:
Decrypt(Encrypt(plaintext, OaepSHA256), OaepSHA256)returns a byte array equal toplaintext. Assert on the bytes; an encrypt-only test passes whether or not anything can read the result back. Do the same throughHybridEncrypt/HybridDecrypt- it is the path where a droppednonceortagproduces ciphertext nothing can open. - Boundary input: on a 3072-bit key, 318 bytes encrypts under
OaepSHA256and 319 throwsCryptographicException. Assert the largest payload the system actually sends is under the limit:OaepSHA256leaves 318 bytes wherePkcs1left 373, and on a 2048-bit key 190 wherePkcs1left 245 (measured on .NET 10). A record that encrypted before the padding change may not after it. - Probabilistic output: two
Encryptcalls on the same plaintext produce different ciphertexts. Equal output means OAEP is not being applied. - Negative case: a ciphertext produced with
OaepSHA256throwsCryptographicExceptionwhen decrypted withRSAEncryptionPadding.Pkcs1rather than returning a shorter or garbled array. - Uniform failures: a tampered ciphertext, one produced for a different key, and a truncated one all reach the caller as the same generic error and status code.
CryptographicException.Messagereaching a response body or a caller-visible log is what makes an oracle usable. - Re-run the scanner that reported the finding and confirm it no longer triggers.
Common Pitfalls
- Calling
rsa.Encrypt(data, true)as the "quick fix" for the boolean overload without migrating toRSA.Create()/RSAEncryptionPadding.OaepSHA256-fOAEP: trueon the legacyRSACryptoServiceProvideralways uses SHA-1 for the OAEP hash (it has no parameter to change it), so the padding-oracle risk is closed but the weak-hash issue is not. - Wrapping
rsa.Decrypt()in a try/catch that logs or returns the underlyingCryptographicExceptionmessage - even with OAEP, exposing whether a failure came from padding validation versus a downstream step can leak information; catch and return a single generic decryption failure instead. - Encrypting a payload just over the ~318-byte OAEP-SHA256 limit by switching to a larger key size (3072/4096-bit) instead of hybrid encryption - this only raises the ceiling and reintroduces the same problem for the next slightly-larger payload; use AES-GCM + RSA-OAEP regardless of key size once payloads are variable-length.
- Importing an
RSACryptoServiceProviderkey intoRSA.Create()viaImportParametersbut leaving old call sites that still constructRSACryptoServiceProviderdirectly elsewhere in the codebase - the key material is portable, but each remainingfOAEP: falsecall site is still independently vulnerable.
Additional Resources
- Bleichenbacher's Attack Paper (1998)
- CWE-780 Details
- NIST SP 800-131A Rev 2: Transitioning the Use of Cryptographic Algorithms and Key Lengths - RSA-2048 provides 112-bit security strength, which is currently acceptable for applying protection
- NIST SP 800-57 Part 1 Rev 5: Recommendation for Key Management - Table 4 is the source for the 2030 date: 112-bit strength is Acceptable for applying protection (generating keys, encrypting, signing) through 2030 and Disallowed from 2031, while processing already-protected data stays permitted as legacy use
- OWASP Cryptographic Storage Cheat Sheet
- OWASP Top 10 2025 A04: Cryptographic Failures
- RFC 8017: PKCS #1 v2.2
- RSAEncryptionPadding Class