CWE-780: Use of RSA Without OAEP - JavaScript/Node.js
Overview
Using RSA encryption without OAEP (Optimal Asymmetric Encryption Padding) enables padding oracle attacks, chosen ciphertext attacks, and message malleability. In Node.js, this commonly occurs when using the crypto module without explicitly specifying OAEP padding, which defaults to the insecure PKCS#1 v1.5 padding.
Primary Defence: Always specify padding: crypto.constants.RSA_PKCS1_OAEP_PADDING with oaepHash set to 'sha256' or stronger when using crypto.publicEncrypt() and crypto.privateDecrypt(), use Web Crypto API's RSA-OAEP algorithm with explicit hash functions in browser environments, never rely on default padding schemes, and consider using hybrid encryption (RSA for key exchange + AES-GCM for data) to prevent padding oracle attacks and ensure cryptographic security.
Common Vulnerable Patterns
Default Padding with crypto.publicEncrypt()
// VULNERABLE - Defaults to RSA_PKCS1_PADDING (insecure!)
const crypto = require('crypto');
const encrypted = crypto.publicEncrypt(
publicKey, // No padding specified
Buffer.from(message)
);
// Uses PKCS#1 v1.5 by default - vulnerable to Bleichenbacher's attack!
Why this is vulnerable:
- Omitting padding in
crypto.publicEncrypt()defaults toRSA_PKCS1_PADDING(PKCS#1 v1.5). - PKCS#1 v1.5 is deterministic, enabling Bleichenbacher-style padding oracles.
- Attackers can adaptively probe decryption success/failure to recover plaintext.
- Identical plaintexts yield identical ciphertexts, enabling pattern leakage.
Using node-forge without OAEP
// VULNERABLE - node-forge with PKCS#1 v1.5 padding
const forge = require('node-forge');
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
const encrypted = publicKey.encrypt(
message,
'RSAES-PKCS1-V1_5' // Insecure padding scheme
);
Why this is vulnerable:
'RSAES-PKCS1-V1_5'implements deprecated PKCS#1 v1.5 padding.- Distinct error messages or timing differences create a padding oracle.
- Attackers can brute-force with crafted ciphertexts and recover plaintext.
- Real-world exploits exist against TLS and messaging systems.
crypto-browserify with Default Padding
// VULNERABLE - crypto-browserify defaults to PKCS#1 v1.5
const crypto = require('crypto-browserify');
const encrypted = crypto.publicEncrypt(publicKeyPem, Buffer.from(message));
// No padding specified - uses insecure default
Why this is vulnerable:
- crypto-browserify defaults to PKCS#1 v1.5 when padding is omitted.
- Browser decryption can leak timing differences, enabling padding oracles.
- Deterministic PKCS#1 v1.5 exposes repeated tokens or keys.
- Client-side environments lack server-grade timing mitigations.
WebCrypto API with PKCS#1 v1.5
// VULNERABLE - Using RSA-PKCS1-v1_5 algorithm
async function encryptInsecure(data, publicKey) {
const encoded = new TextEncoder().encode(data);
const ciphertext = await crypto.subtle.encrypt(
{
name: 'RSA-PKCS1-v1_5' // Insecure!
},
publicKey,
encoded
);
return ciphertext;
}
Why this is vulnerable:
'RSA-PKCS1-v1_5'uses obsolete PKCS#1 v1.5 padding.- Promise timing differences can reveal padding validity.
- Browser APIs (e.g.,
performance.now()) enable high-precision timing oracles. - XSS or third-party scripts can probe decryptions at scale.
Using Weak Hash with OAEP
// VULNERABLE - SHA-1 is deprecated
const crypto = require('crypto');
const encrypted = crypto.publicEncrypt(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha1' // Weak! Use sha256 or better
},
Buffer.from(message)
);
Why this is vulnerable:
- OAEP is fine, but
SHA-1is broken (practical collisions since 2017). - OAEP depends on a strong hash; SHA-1 undermines that security proof.
- 160-bit SHA-1 yields only ~80-bit collision security (too weak).
- Standards and compliance regimes prohibit SHA-1 in new systems.
Secure Patterns
Node.js crypto Module with RSA-OAEP (PREFERRED)
Always specify RSA_PKCS1_OAEP_PADDING when encrypting with RSA.
// SECURE - Node.js crypto with OAEP padding
const crypto = require('crypto');
// Generate RSA key pair
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048, // Minimum 2048 bits
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
const message = 'Sensitive data to encrypt';
// SECURE - Encrypt with OAEP padding
const encrypted = crypto.publicEncrypt(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256' // Use SHA-256 or better
},
Buffer.from(message, 'utf8')
);
// SECURE - Decrypt with OAEP padding
const decrypted = crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
encrypted
);
console.log('Original:', message);
console.log('Decrypted:', decrypted.toString('utf8'));
Why this works:
RSA_PKCS1_OAEP_PADDINGmakes RSA probabilistic, blocking ciphertext replay.oaepHash: 'sha256'provides strong collision resistance for OAEP.- Node.js follows RFC 8017, ensuring cross-platform compatibility.
- OAEP eliminates PKCS#1 v1.5 padding oracles.
WebCrypto API with RSA-OAEP (Browser & Node.js 15.6+)
// SECURE - WebCrypto API with RSA-OAEP
const { webcrypto } = require('crypto');
const { subtle } = webcrypto;
async function generateKeyPairOAEP() {
return await subtle.generateKey(
{
name: 'RSA-OAEP', // OAEP algorithm
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]), // 65537
hash: 'SHA-256' // Strong hash
},
true, // extractable
['encrypt', 'decrypt']
);
}
async function encryptWithOAEP(data, publicKey) {
const encoded = new TextEncoder().encode(data);
const ciphertext = await subtle.encrypt(
{
name: 'RSA-OAEP'
},
publicKey,
encoded
);
return Buffer.from(ciphertext);
}
async function decryptWithOAEP(ciphertext, privateKey) {
const plaintext = await subtle.decrypt(
{
name: 'RSA-OAEP'
},
privateKey,
ciphertext
);
return new TextDecoder().decode(plaintext);
}
// Example usage
(async () => {
const keyPair = await generateKeyPairOAEP();
const message = 'Secret message';
const encrypted = await encryptWithOAEP(message, keyPair.publicKey);
const decrypted = await decryptWithOAEP(encrypted, keyPair.privateKey);
console.log('Original:', message);
console.log('Decrypted:', decrypted);
})();
Why this works:
- Keys are generated for
RSA-OAEPand cannot be used with PKCS#1 v1.5. - SHA-256 is bound to the key, keeping encrypt/decrypt consistent.
- WebCrypto uses native crypto providers and avoids key extraction.
- Async APIs integrate cleanly while keeping crypto in vetted implementations.
node-forge Library with OAEP
// SECURE - node-forge with RSA-OAEP
const forge = require('node-forge');
// Generate key pair
const keypair = forge.pki.rsa.generateKeyPair({ bits: 2048 });
const publicKey = keypair.publicKey;
const privateKey = keypair.privateKey;
const message = 'Confidential data';
// SECURE - Encrypt with OAEP
const encrypted = publicKey.encrypt(
message,
'RSA-OAEP',
{
md: forge.md.sha256.create(), // Use SHA-256
mgf1: {
md: forge.md.sha256.create() // MGF1 with SHA-256
}
}
);
// SECURE - Decrypt with OAEP
const decrypted = privateKey.decrypt(
encrypted,
'RSA-OAEP',
{
md: forge.md.sha256.create(),
mgf1: {
md: forge.md.sha256.create()
}
}
);
console.log('Original:', message);
console.log('Decrypted:', decrypted);
Why this works:
'RSA-OAEP'forces OAEP padding instead of PKCS#1 v1.5.- Explicit
mdandmgf1.mdkeep hash selection under your control. - Prevents silent fallback to insecure defaults.
- Works in environments without native crypto (with a performance trade-off).
crypto-browserify with OAEP
// SECURE - crypto-browserify with explicit OAEP padding
const crypto = require('crypto-browserify');
const encrypted = crypto.publicEncrypt(
{
key: publicKeyPem,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
Buffer.from(message)
);
const decrypted = crypto.privateDecrypt(
{
key: privateKeyPem,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
encrypted
);
Why this works:
- Same API as Node.js (
RSA_PKCS1_OAEP_PADDING,oaepHash) for drop-in use. - Uses WebCrypto when available, falls back to JS otherwise.
- Explicit padding/hash avoids environment-specific defaults.
- Keeps security consistent across Node and browser builds.
Hybrid Encryption for Large Data
// SECURE - Hybrid encryption (RSA-OAEP + AES-GCM)
const crypto = require('crypto');
class HybridEncryption {
static encrypt(data, rsaPublicKey) {
// Step 1: Generate random AES-256 key
const aesKey = crypto.randomBytes(32); // 256 bits
// Step 2: Encrypt data with AES-GCM
const iv = crypto.randomBytes(12); // 96-bit IV for GCM
const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv);
let ciphertext = cipher.update(data, 'utf8');
ciphertext = Buffer.concat([ciphertext, cipher.final()]);
const authTag = cipher.getAuthTag();
// Step 3: Encrypt AES key with RSA-OAEP
const encryptedKey = crypto.publicEncrypt(
{
key: rsaPublicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
aesKey
);
return {
encryptedKey: encryptedKey,
iv: iv,
authTag: authTag,
ciphertext: ciphertext
};
}
static decrypt(encryptedData, rsaPrivateKey) {
// Step 1: Decrypt AES key with RSA-OAEP
const aesKey = crypto.privateDecrypt(
{
key: rsaPrivateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
encryptedData.encryptedKey
);
// Step 2: Decrypt data with AES-GCM
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
aesKey,
encryptedData.iv
);
decipher.setAuthTag(encryptedData.authTag);
let plaintext = decipher.update(encryptedData.ciphertext);
plaintext = Buffer.concat([plaintext, decipher.final()]);
return plaintext.toString('utf8');
}
}
// Example: Encrypt large data (megabytes)
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
const largeData = 'A'.repeat(1000000);
const encrypted = HybridEncryption.encrypt(largeData, publicKey);
const decrypted = HybridEncryption.decrypt(encrypted, privateKey);
console.log('Data size:', largeData.length);
console.log('Match:', largeData === decrypted);
Why this works:
- RSA size limits make it unsuitable for bulk data.
- AES-GCM encrypts large payloads quickly and provides integrity checks.
- RSA-OAEP securely wraps the short AES key.
- Mirrors TLS design for performance and security at scale.
Remediation Strategy
PRIORITY 1: Use Node.js crypto with OAEP Padding (PRIMARY FIX)
Always specify RSA_PKCS1_OAEP_PADDING when encrypting with RSA.
Basic RSA-OAEP Encryption/Decryption
const crypto = require('crypto');
// Generate RSA key pair
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048, // Minimum 2048 bits
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
const message = 'Sensitive data to encrypt';
// SECURE - Encrypt with OAEP padding
const encrypted = crypto.publicEncrypt(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256' // Use SHA-256 or better
},
Buffer.from(message, 'utf8')
);
// SECURE - Decrypt with OAEP padding
const decrypted = crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
encrypted
);
console.log('Original:', message);
console.log('Decrypted:', decrypted.toString('utf8'));
Using async/await (Node.js 15.6+)
const { webcrypto } = require('crypto');
const { subtle } = webcrypto;
async function generateKeyPairAsync() {
return await subtle.generateKey(
{
name: 'RSA-OAEP',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]), // 65537
hash: 'SHA-256'
},
true, // extractable
['encrypt', 'decrypt']
);
}
async function encryptWithOAEP(data, publicKey) {
const encoded = new TextEncoder().encode(data);
const ciphertext = await subtle.encrypt(
{
name: 'RSA-OAEP'
},
publicKey,
encoded
);
return Buffer.from(ciphertext);
}
async function decryptWithOAEP(ciphertext, privateKey) {
const plaintext = await subtle.decrypt(
{
name: 'RSA-OAEP'
},
privateKey,
ciphertext
);
return new TextDecoder().decode(plaintext);
}
// Example usage
(async () => {
const keyPair = await generateKeyPairAsync();
const message = 'Secret message';
const encrypted = await encryptWithOAEP(message, keyPair.publicKey);
const decrypted = await decryptWithOAEP(encrypted, keyPair.privateKey);
console.log('Original:', message);
console.log('Decrypted:', decrypted);
})();
PRIORITY 2: Hybrid Encryption for Large Data
RSA is limited to small data (~190 bytes for 2048-bit RSA with SHA-256). Use hybrid encryption for larger data.
const crypto = require('crypto');
class HybridEncryption {
static encrypt(data, rsaPublicKey) {
// Step 1: Generate random AES-256 key
const aesKey = crypto.randomBytes(32); // 256 bits
// Step 2: Encrypt data with AES-GCM
const iv = crypto.randomBytes(12); // 96-bit IV for GCM
const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv);
let ciphertext = cipher.update(data, 'utf8');
ciphertext = Buffer.concat([ciphertext, cipher.final()]);
const authTag = cipher.getAuthTag();
// Step 3: Encrypt AES key with RSA-OAEP
const encryptedKey = crypto.publicEncrypt(
{
key: rsaPublicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
aesKey
);
return {
encryptedKey: encryptedKey,
iv: iv,
authTag: authTag,
ciphertext: ciphertext
};
}
static decrypt(encryptedData, rsaPrivateKey) {
// Step 1: Decrypt AES key with RSA-OAEP
const aesKey = crypto.privateDecrypt(
{
key: rsaPrivateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
encryptedData.encryptedKey
);
// Step 2: Decrypt data with AES-GCM
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
aesKey,
encryptedData.iv
);
decipher.setAuthTag(encryptedData.authTag);
let plaintext = decipher.update(encryptedData.ciphertext);
plaintext = Buffer.concat([plaintext, decipher.final()]);
return plaintext.toString('utf8');
}
}
// Example usage
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
// Can encrypt large data (megabytes)
const largeData = 'A'.repeat(1000000);
const encrypted = HybridEncryption.encrypt(largeData, publicKey);
const decrypted = HybridEncryption.decrypt(encrypted, privateKey);
console.log('Data size:', largeData.length);
console.log('Match:', largeData === decrypted);
PRIORITY 3: RSA Signatures with PSS (Not OAEP)
For signatures (not encryption), use PSS padding instead of OAEP.
const crypto = require('crypto');
function signWithPSS(message, privateKey) {
const sign = crypto.createSign('RSA-SHA256');
sign.update(message);
sign.end();
// Sign with PSS padding
return sign.sign({
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN
});
}
function verifyWithPSS(message, signature, publicKey) {
const verify = crypto.createVerify('RSA-SHA256');
verify.update(message);
verify.end();
return verify.verify({
key: publicKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_AUTO
}, signature);
}
// Example
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
const message = 'Document to sign';
const signature = signWithPSS(message, privateKey);
const isValid = verifyWithPSS(message, signature, publicKey);
console.log('Signature valid:', isValid);
Complete Working Example with Key Storage
const crypto = require('crypto');
const fs = require('fs');
class RSAKeyManager {
// Generate and save key pair
static generateAndSaveKeys(publicKeyFile, privateKeyFile) {
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
fs.writeFileSync(publicKeyFile, publicKey);
fs.writeFileSync(privateKeyFile, privateKey);
return { publicKey, privateKey };
}
// Load keys from files
static loadKeys(publicKeyFile, privateKeyFile) {
const publicKey = fs.readFileSync(publicKeyFile, 'utf8');
const privateKey = fs.readFileSync(privateKeyFile, 'utf8');
return { publicKey, privateKey };
}
// Encrypt with OAEP
static encrypt(message, publicKey) {
return crypto.publicEncrypt(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
Buffer.from(message, 'utf8')
);
}
// Decrypt with OAEP
static decrypt(ciphertext, privateKey) {
return crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
ciphertext
).toString('utf8');
}
}
// Usage
const publicKeyFile = 'public.pem';
const privateKeyFile = 'private.pem';
// Generate keys
RSAKeyManager.generateAndSaveKeys(publicKeyFile, privateKeyFile);
// Load keys
const { publicKey, privateKey } = RSAKeyManager.loadKeys(
publicKeyFile,
privateKeyFile
);
// Encrypt
const message = 'Confidential data';
const encrypted = RSAKeyManager.encrypt(message, publicKey);
console.log('Encrypted:', encrypted.toString('base64'));
// Decrypt
const decrypted = RSAKeyManager.decrypt(encrypted, privateKey);
console.log('Decrypted:', decrypted);
TypeScript Version
import * as crypto from 'crypto';
interface EncryptedData {
encryptedKey: Buffer;
iv: Buffer;
authTag: Buffer;
ciphertext: Buffer;
}
interface KeyPair {
publicKey: string;
privateKey: string;
}
class SecureRSA {
static generateKeyPair(): KeyPair {
return crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
}
static encryptOAEP(message: string, publicKey: string): Buffer {
return crypto.publicEncrypt(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
Buffer.from(message, 'utf8')
);
}
static decryptOAEP(ciphertext: Buffer, privateKey: string): string {
return crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
ciphertext
).toString('utf8');
}
}
// Usage
const keys = SecureRSA.generateKeyPair();
const encrypted = SecureRSA.encryptOAEP('Secret', keys.publicKey);
const decrypted = SecureRSA.decryptOAEP(encrypted, keys.privateKey);
Migration from PKCS#1 v1.5 to OAEP
const crypto = require('crypto');
class MigrationHelper {
// Dual-padding decryption for migration period
static decryptWithFallback(ciphertext, privateKey) {
// Try OAEP first (secure)
try {
return crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
ciphertext
).toString('utf8');
} catch (error) {
console.warn('OAEP decryption failed, trying legacy PKCS1 padding');
}
// Fall back to legacy PKCS#1 v1.5
try {
return crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PADDING
},
ciphertext
).toString('utf8');
} catch (error) {
throw new Error(`Decryption failed with both OAEP and PKCS1: ${error.message}`);
}
}
// Always encrypt with OAEP (never use PKCS1 for new data)
static encryptSecure(message, publicKey) {
return crypto.publicEncrypt(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
Buffer.from(message, 'utf8')
);
}
}
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
1. Locate the Finding
Review the data flow in the security scan report:
- Source: Where encryption input originates (user data, API requests, file contents)
- Sink: The RSA encryption call (
crypto.publicEncrypt(),subtle.encrypt(),forge.publicKey.encrypt()) - Note any frames between source and sink
Understand the Data Flow
Trace how data moves from source to sink:
- Identify the encryption method being used
- Check if padding is specified explicitly
- Look for library imports (
crypto,node-forge,crypto-browserify) - Verify the padding constant or algorithm name
Identify the Vulnerable Pattern
Match the code to patterns above:
| Pattern | Vulnerability | Fix |
|---|---|---|
crypto.publicEncrypt(publicKey, data) |
Missing padding spec → defaults to PKCS#1 v1.5 | Add RSA_PKCS1_OAEP_PADDING |
padding: crypto.constants.RSA_PKCS1_PADDING |
Explicit PKCS#1 v1.5 | Change to RSA_PKCS1_OAEP_PADDING |
oaepHash: 'sha1' |
Weak hash with OAEP | Change to sha256 or stronger |
publicKey.encrypt(msg, 'RSAES-PKCS1-V1_5') |
node-forge PKCS#1 v1.5 | Change to 'RSA-OAEP' |
{ name: 'RSA-PKCS1-v1_5' } |
WebCrypto PKCS#1 v1.5 | Change to 'RSA-OAEP' |
Apply the Fix
Choose the appropriate remediation approach based on your requirements:
Option 1: Add OAEP padding to Node.js crypto (Recommended - most common fix)
- Always specify
padding: crypto.constants.RSA_PKCS1_OAEP_PADDINGin options object - Set
oaepHashto'sha256'or stronger (never use'sha1') - Update both encryption (
publicEncrypt) and decryption (privateDecrypt) calls - Ensure padding parameters match between encryption and decryption operations
Option 2: Use WebCrypto API with RSA-OAEP (Browser and modern Node.js)
- Specify
name: 'RSA-OAEP'in algorithm parameter during key generation - Set
hash: 'SHA-256'or stronger in key generation options - Use
subtle.encrypt()andsubtle.decrypt()with RSA-OAEP keys - Works in browsers (HTTPS only) and Node.js 15.6+
Option 3: Use hybrid encryption for large data (Data > 190 bytes)
- RSA has strict size limits based on key length and padding overhead
- Generate ephemeral AES-256 key for symmetric encryption of bulk data
- Encrypt AES key with RSA-OAEP, encrypt data with AES-GCM
- Combine encrypted key, IV, auth tag, and ciphertext for transport
Option 4: Update third-party library configuration (node-forge, crypto-browserify)
- For node-forge: Use
'RSA-OAEP'scheme withmd: forge.md.sha256.create() - For crypto-browserify: Same options as Node.js crypto module
- Ensure MGF1 hash function matches main hash for OAEP
- Avoid
'RSAES-PKCS1-V1_5'scheme in all cases
See the Secure Patterns section for detailed implementation examples of each approach.
Verify the Fix
Test probabilistic encryption:
const msg = 'test';
const enc1 = crypto.publicEncrypt({
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
}, Buffer.from(msg));
const enc2 = crypto.publicEncrypt({
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
}, Buffer.from(msg));
// OAEP should produce different ciphertexts
console.assert(!enc1.equals(enc2), 'OAEP should be probabilistic');
Run tests:
Rescan with the security scanner:
- Re-scan the updated code
- Confirm CWE-780 finding is resolved
Check for Similar Issues
Search your codebase for related vulnerabilities:
# Search for RSA encryption calls
grep -r "publicEncrypt" .
grep -r "RSA-PKCS1-v1_5" .
grep -r "RSAES-PKCS1-V1_5" .
grep -r "RSA_PKCS1_PADDING" .
# Check for missing padding specifications
grep -r "publicEncrypt(publicKey" .
Common Finding Examples
Example 1: Node.js crypto with default padding
// security finding: CWE-780
// Source: request.body.data
// Sink: crypto.publicEncrypt()
// VULNERABLE CODE:
const data = req.body.data;
const encrypted = crypto.publicEncrypt(publicKey, Buffer.from(data));
// FIX:
const data = req.body.data;
const encrypted = crypto.publicEncrypt(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
Buffer.from(data)
);
Example 2: node-forge with PKCS#1 v1.5
// security finding: CWE-780
// Source: config.secretData
// Sink: publicKey.encrypt()
// VULNERABLE CODE:
const forge = require('node-forge');
const encrypted = publicKey.encrypt(secretData, 'RSAES-PKCS1-V1_5');
// FIX:
const forge = require('node-forge');
const encrypted = publicKey.encrypt(
secretData,
'RSA-OAEP',
{
md: forge.md.sha256.create(),
mgf1: { md: forge.md.sha256.create() }
}
);
Example 3: WebCrypto API with wrong algorithm
// security finding: CWE-780
// Source: userInput
// Sink: subtle.encrypt()
// VULNERABLE CODE:
async function encryptData(data, publicKey) {
const encoded = new TextEncoder().encode(data);
return await crypto.subtle.encrypt(
{ name: 'RSA-PKCS1-v1_5' }, // Insecure!
publicKey,
encoded
);
}
// FIX:
async function encryptData(data, publicKey) {
const encoded = new TextEncoder().encode(data);
return await crypto.subtle.encrypt(
{ name: 'RSA-OAEP' }, // Secure!
publicKey,
encoded
);
}