VNX-JAVA-018 – Java RSA cipher without OAEP padding

Overview

This rule flags calls to Cipher.getInstance() that specify an RSA transformation string containing PKCS1Padding, NoPadding, or the shorthand RSA/ECB/PKCS1. These padding schemes are cryptographically weak and expose applications to a class of attack known as the Bleichenbacher padding oracle attack (also called the “million message attack”). The vulnerability is tracked as CWE-780 (Use of RSA Algorithm without OAEP).

PKCS#1 v1.5 padding prepends a structured sequence of bytes to the plaintext before encryption. The structure is deterministic enough that a server decrypting a modified ciphertext and responding differently based on whether the padding is valid leaks a single bit of information per query. Bleichenbacher showed in 1998 that an attacker who can distinguish “valid padding” from “invalid padding” — even through indirect means such as differing error messages, timing variations, or TLS alert types — can decrypt any RSA ciphertext with approximately one million adaptive chosen-ciphertext queries. Despite being over two decades old, this attack remains practical and has been rediscovered repeatedly in TLS implementations (ROBOT attack, 2017).

OAEP (Optimal Asymmetric Encryption Padding) provides probabilistic encryption and is provably secure in the random oracle model. It does not have a padding oracle vulnerability.

Severity: High | CWE: CWE-780 – Use of RSA Algorithm without OAEP

Why This Matters

The ROBOT (Return Of Bleichenbacher’s Oracle Threat) vulnerability, disclosed in 2017, demonstrated that 27 of the top 100 Alexa websites — including Facebook, PayPal, and major US government sites — were vulnerable to the 1998 Bleichenbacher attack. An attacker with the ability to make adaptive queries against the TLS handshake could decrypt TLS session keys encrypted under RSA PKCS#1 v1.5, breaking the confidentiality of past and present sessions.

In application code, the risk is equally concrete. An API that uses RSA PKCS#1 v1.5 to decrypt tokens, license keys, or session data, and that returns different HTTP status codes or error messages depending on whether decryption succeeded, provides the oracle an attacker needs. The attacker doesn’t need to access your private key directly — they use your own server as a decryption oracle to recover the plaintext incrementally.

No-padding mode (NoPadding) is even more dangerous: it provides textbook RSA with no randomness, making it trivially malleable. Multiplying a ciphertext by the encryption of a known value produces a ciphertext that decrypts to the plaintext multiplied by the known value — a property that enables chosen-ciphertext attacks without any oracle at all.

What Gets Flagged

// FLAGGED: PKCS#1 v1.5 padding is vulnerable to padding oracle attacks
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] plaintext = cipher.doFinal(ciphertext);

// FLAGGED: no padding is textbook RSA — fully malleable
Cipher cipher = Cipher.getInstance("RSA/NONE/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);

Remediation

  1. Replace PKCS1Padding and NoPadding with OAEPWithSHA-256AndMGF1Padding in all Cipher.getInstance() calls that use RSA.
  2. For new code, prefer OAEPWithSHA-512AndMGF1Padding if your key size is 4096 bits or larger.
  3. Do not catch BadPaddingException and IllegalBlockSizeException separately and return different responses — always treat any decryption failure identically to prevent timing or error-message oracles.
  4. Consider migrating bulk encryption to hybrid encryption: use RSA-OAEP to encrypt only an ephemeral AES key, then use AES-GCM for the actual data.
// SAFE: OAEP padding prevents padding oracle attacks
import javax.crypto.Cipher;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import java.security.spec.MGF1ParameterSpec;

OAEPParameterSpec oaepSpec = new OAEPParameterSpec(
    "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT
);

Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey, oaepSpec);

byte[] plaintext;
try {
    plaintext = cipher.doFinal(ciphertext);
} catch (Exception e) {
    // Always return the same error regardless of failure reason
    throw new CryptoException("Decryption failed");
}

References