Serge Vaudenay introduced the padding oracle attack in 2002. The setting was modest: AES-CBC with PKCS#7 padding, an attacker who could submit ciphertexts and observe whether decryption succeeded or failed. From that minimal capability, Vaudenay showed how to decrypt arbitrary intercepted ciphertexts without ever holding the key.
The attack has since shown up everywhere CBC-mode encryption with separate-then-verify integrity checking exists: ASP.NET ViewState, JSF, multiple TLS vulnerabilities (BEAST, Lucky 13, POODLE), RSA's PKCS#1 v1.5 padding (Bleichenbacher), countless homegrown cookie encryption schemes. The underlying logic is the same every time.
The Setup: CBC, Padding, and Why It's Fragile
CBC — Cipher Block Chaining — encrypts a plaintext block by XORing it with the previous ciphertext block (or the IV for the first block) before passing it through the block cipher. Decryption reverses this: the block cipher inverse is applied, then the previous ciphertext block is XORed in to recover the plaintext.
Because AES operates on 16-byte blocks but plaintexts have arbitrary lengths, padding fills out the last block. PKCS#7 padding does this with bytes whose value equals the number of padding bytes — so a plaintext needing 3 bytes of padding ends with 03 03 03, a plaintext needing 7 bytes ends with 07 07 07 07 07 07 07, and so on. Exactly one block of 10 10 10 ... if the plaintext aligned.
The decryption side checks padding. If the trailing bytes don't form a valid PKCS#7 pattern, padding is rejected. That rejection is the oracle.
"This ciphertext decrypted to invalid padding" looks like a benign error. It is not. Given that single bit, repeated 256 times per byte, an attacker can decrypt your entire message.
Walking the Attack
Here's the per-byte trick. Take a ciphertext block C2 whose plaintext P2 we want to recover. CBC's structure means P2 = D(C2) XOR C1 — where D is the block cipher's inverse and C1 is the prior ciphertext block.
The attacker replaces C1 with a manipulated value C1' and submits the modified ciphertext C1' || C2. The system decrypts C2, XORs with C1' to produce a candidate plaintext P2', and reports valid or invalid padding.
- Set the last byte of
C1'to0x00through0xFFin turn. - Eventually one value makes the system report "valid padding."
- Almost certainly that means the last byte of
P2'equals0x01— a one-byte padding. - From
P2' = D(C2) XOR C1'we now knowD(C2)'s last byte: it equals0x01 XOR C1'[15]. - And the original last byte of
P2equalsD(C2)[15] XOR C1[15].
One byte recovered. Now adjust to attack the second-to-last byte: set the last byte of C1' so that the decrypted last byte equals 0x02, then iterate the second-to-last byte until you find a valid padding (a candidate 02 02). Repeat for all sixteen bytes of the block. Then move on to the next block.
Average work: ~128 queries per byte, ~2048 per block. For a 1 KB plaintext, well under 200,000 oracle queries — minutes to hours over a network. The attacker recovers the entire plaintext without ever guessing the key.
The Famous Examples
POODLE (CVE-2014-3566)
POODLE — Padding Oracle On Downgraded Legacy Encryption — exploited SSL 3.0's CBC padding, which (unlike TLS) did not verify every padding byte. An attacker who could force a browser to downgrade to SSL 3.0 (via the "fallback" mechanism that nobody should have shipped) and inject chosen plaintext could recover session cookies one byte at a time. POODLE retired SSL 3.0 across the web in 2014.
Lucky 13 (CVE-2013-0169)
A timing-based padding oracle in TLS's MAC-then-encrypt construction. Even though TLS rejected malformed padding and malformed MACs identically, the time it took to verify the MAC depended on the padding byte length — leaking ~1 bit per request through timing. Mitigation required constant-time MAC verification.
Bleichenbacher (1998 and later)
Daniel Bleichenbacher's 1998 attack on RSA PKCS#1 v1.5 padding is the spiritual ancestor: same shape, applied to RSA encryption rather than CBC. It led to a series of follow-up attacks (DROWN, ROBOT, Marvin) that kept finding the same vulnerability in TLS implementations across two decades. RSA PKCS#1 v1.5 should not exist in new designs.
ASP.NET ViewState (CVE-2010-3332)
Microsoft's ViewState encryption used CBC with a separate HMAC verification that was sometimes disabled or misconfigured. Attackers could submit modified ViewState payloads and observe error pages that distinguished padding failures from later failures. Full plaintext recovery, then full server compromise via deserialization. The fix took years to fully deploy.
The Defense: Don't Build Oracles
The right mental model isn't "make my oracle quieter." It's "stop using constructions that have oracles in them."
| Construction | Padding oracle status |
|---|---|
| CBC + PKCS#7 + verify-after-decrypt | Oracle by default |
| CBC + PKCS#7 + Encrypt-then-MAC (constant-time) | Safe if MAC is checked first and timing is constant |
| AES-GCM (AEAD) | No padding; integrity built-in |
| ChaCha20-Poly1305 (AEAD) | No padding; integrity built-in |
| AES-SIV (deterministic AEAD) | No padding; integrity built-in |
| RSA-OAEP | Better than PKCS#1 v1.5; still implementation-sensitive |
| RSA PKCS#1 v1.5 encryption | Bleichenbacher family; do not use |
Use AEAD. Use AEAD. Use AEAD. There is no scenario in modern application cryptography where you should be hand-assembling CBC plus a MAC. The combined primitives exist specifically to remove this footgun.
If You Inherit CBC Code
Sometimes you're stuck — legacy wire formats, hardware that only supports CBC, vendor protocols you can't change. The rules:
- Encrypt-then-MAC, always. Compute the MAC over the ciphertext (including IV), verify the MAC first, only then decrypt. Padding errors after MAC verification are impossible because invalid ciphertext fails the MAC.
- Constant-time MAC verification. A timing oracle is still an oracle. Use a constant-time comparison function — not
memcmp. - Single error type. "Decryption failed" — same message, same response code, same response time, regardless of what failed.
- Don't log padding details. "Invalid padding byte at offset 15" in an error log becomes an oracle if an attacker can read logs.
What This Means for Real Systems
Padding oracle attacks survive because the defensive pattern (Encrypt-then-MAC, constant-time everything) is subtle and the offensive pattern (a single bit of leaked validity) is obvious. Whenever a designer assumes "the integrity check protects me" without rigorously checking the order and timing of validation steps, the oracle reappears.
Haven's symmetric encryption is AES-256-GCM throughout — chosen specifically so this entire class of attacks does not apply. For asymmetric operations we use modern AEAD-wrapped constructions including HPKE, not RSA PKCS#1 v1.5. The cost of using the right primitive is zero; the cost of using the wrong one is twenty-five years of CVEs.