Blog/Cryptography
CryptographyJuly 25, 2026 · 7 min read

Padding Oracle Attacks: Decrypting Without the Key

Here's the unsettling part: a padding oracle attack decrypts your data without the key. It doesn't break AES. It abuses a server that leaks one bit — whether a ciphertext decrypted to valid padding — and turns that single bit, asked thousands of times, into the full plaintext.

It works against CBC mode, which pads plaintext to a block boundary (PKCS#7) and, on decryption, checks that the padding is well-formed. If the server responds differently for "valid padding" versus "invalid padding" — a different error, status code, or even response time — that difference is the oracle.

How one bit becomes the whole message

CBC decryption XORs each decrypted block with the previous ciphertext block. That means an attacker who controls the previous block controls the final plaintext bytes:

  1. Take a target ciphertext block. Prepend an attacker-chosen block in front of it.
  2. Brute-force the last byte of the chosen block (256 values) until the server reports valid padding — meaning the decrypted last byte became 0x01.
  3. That reveals the intermediate decryption value of that byte (intermediate = chosen_byte XOR 0x01), and therefore the real plaintext byte (plaintext = intermediate XOR real_previous_byte).
  4. Move to the next byte (targeting padding 0x02 0x02, then 0x03 0x03 0x03…) and repeat.

Roughly 256 requests per byte recovers the entire message. No key, no AES weakness — just a server that answered "is the padding OK?" one too many times. The same primitive (flipping previous-block bytes) also lets an attacker forge chosen plaintext, which is how real breaches turned padding oracles into full account takeover.

Where oracles hide

  • Encrypted session tokens, cookies, or "state" blobs decrypted server-side (the classic target)
  • Encrypted URL parameters and viewstate
  • Any CBC decryption that returns a distinguishable error, status, or timing for bad padding
  • Message queues or caches storing CBC ciphertext that a request can influence

The fix: authenticated encryption, so tampered ciphertext is never decrypted

The root problem is decrypting attacker-modified ciphertext and revealing anything about the result. Authenticated encryption (AEAD) fixes it at the source: it verifies an integrity tag before decrypting, so a tampered ciphertext is rejected with an identical, uninformative error — there is no padding step to probe.

const crypto = require('crypto');

// Use AES-256-GCM: it authenticates, then decrypts. Tampering fails the
// tag check, so there is no padding oracle to exploit.
function encrypt(plaintext, key) {
const iv = crypto.randomBytes(12);              // 96-bit nonce for GCM
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();                // integrity tag
return Buffer.concat([iv, tag, ct]).toString('base64');
}

function decrypt(blob, key) {
const data = Buffer.from(blob, 'base64');
const iv = data.subarray(0, 12);
const tag = data.subarray(12, 28);
const ct = data.subarray(28);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
// If the ciphertext was modified, final() THROWS on tag mismatch —
// one uniform error, no padding signal, nothing to probe.
return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
}

The principles

  • Use AEAD by default. AES-GCM or ChaCha20-Poly1305 authenticate before decrypting, which removes the oracle entirely. This is the right default for all application encryption.
  • If you must use CBC, Encrypt-then-MAC. Verify an HMAC over the ciphertext in constant time before decrypting, and reject on failure. Never decrypt first and reveal padding results.
  • Make errors uniform. Return one identical, generic error for any decryption failure — no distinct "bad padding" message, status, or timing.
  • Don't hand-roll crypto. Use a vetted library's AEAD interface and let it manage nonces and tags.

The one line: never let an attacker learn anything about the result of decrypting data they modified — authenticate first, and a padding oracle can't exist.

Try it in the padding oracle simulation: decrypt a CBC token byte by byte using only padding responses, then switch to AES-GCM and watch the oracle disappear. It pairs with why you should never roll your own crypto — this is exactly the kind of subtlety hand-built crypto misses.

Share this post

Frequently Asked Questions

Related posts

Timing Attacks: When == Leaks Your Secret One Byte at a Time

A string comparison that returns early tells an attacker how many characters they got right. Here's how timing attacks extract tokens and passwords, and how constant-time comparison shuts them down.

Jul 21, 2026 · 6 min read

EC Keys and ECDSA: The Faster Alternative to RSA for JWT Signing

Learn how elliptic curve keys work, why P-256 is the right default for JWT ES256, and how ECDSA compares to RSA in size, speed, and security.

Mar 13, 2026 · 6 min read

AES vs RSA — Which Encryption Should You Use in Your App?

A practical guide for backend developers on when to use AES symmetric encryption vs RSA asymmetric encryption. Includes real-world use cases, code examples in JavaScript, Python, Java, PHP, Kotlin, and Swift.

Mar 12, 2026 · 10 min read