What Is a Padding Oracle Attack and How It Decrypts CBC Without the Key

What Is a Padding Oracle Attack and How It Decrypts CBC Without the Key

Written by

in

A padding oracle attack lets someone decrypt CBC encrypted data without ever knowing the key, using nothing but a single bit of feedback the system was never supposed to give away. The attacker submits a ciphertext, the system tries to decrypt it, and the system tells the sender one thing it should have kept to itself: whether the padding came out valid. That one bit, asked over and over against tweaked ciphertext, is enough to peel the plaintext apart one byte at a time, and even to forge ciphertext that decrypts to a message the attacker chose. The leak does not have to be an explicit error. A status code, a timing difference, or a connection that drops a hair faster is the same bit by another name. This post walks the mechanism from the ground up: how CBC chains its blocks, why messages get padded, where the oracle hides, the byte at a time math that turns it into a full decryption, and the real attacks that took this from a 2002 paper to a protocol wide emergency.

What a padding oracle attack actually is

A padding oracle attack is a chosen ciphertext attack against a block cipher running in CBC mode. The target is not the cipher itself. AES is not broken here, and neither is the key. The target is a small piece of behavior wrapped around the cipher: the part that, after decrypting, checks whether the padding bytes at the end of the message are well formed and reacts differently when they are not. An oracle, in the cryptographic sense, is any function an attacker can query that answers a yes or no question about a secret. Here the question is just is this padding valid, and the answer, leaked through any side channel at all, is the lever that pries the whole message open.

To see how a yes or no about padding becomes a full decryption, you have to look at two pieces working together: how block ciphers pad messages, and how CBC mode chains its blocks. Neither is dangerous alone. The danger is in the seam between them.

CBC mode and why padding exists

A block cipher encrypts a fixed size chunk at a time. AES works on 16 byte blocks and nothing else. Feed it 16 bytes, get 16 bytes back. But real messages are not tidy multiples of 16. A session cookie might be 30 bytes, a form field 7 bytes. Something has to stretch the message out to a whole number of blocks before the cipher can touch it, and that something is padding.

The most common scheme is PKCS#7. The rule is simple and self describing: figure out how many bytes you need to reach the next block boundary, call it N, and append N bytes each holding the value N. Need 4 bytes to fill the block, you append 04 04 04 04. Need 1 byte, you append a single 0x01. If the message already lands exactly on a boundary, you add a whole extra block of 16 16 16 ... 16 so that there is always padding to strip and the receiver is never guessing. On the way back out, the receiver reads the value of the final byte, say it is N, checks that the last N bytes all equal N, and lops them off. If those bytes do not form a valid pattern, the padding is wrong, and the receiver knows the message was malformed.

That validity check is the seed of the whole problem. It is a test the receiver runs on attacker supplied bytes, and it has exactly two outcomes.

How CBC chains the blocks

CBC stands for cipher block chaining, and the chaining is the part that matters. You cannot just encrypt each block on its own, because identical plaintext blocks would produce identical ciphertext blocks and leak the structure of the message. CBC fixes this by mixing each plaintext block with the ciphertext of the block before it. If you are still building intuition for how plaintext, ciphertext, and XOR relate before tackling a modern mode like CBC, our free classical cipher solver lets you experiment with substitution ciphers and common encodings by hand, a learning aid for the basics rather than anything that touches the attack below.

Encryption walks the blocks in order. Before a plaintext block P[i] is handed to the cipher, it is XORed with the previous ciphertext block C[i-1]. The very first block has no predecessor, so it is XORed with a random initialization vector, the IV, which travels alongside the ciphertext. In symbols:

C[i] = AES_encrypt( P[i] XOR C[i-1] )
P[i] = AES_decrypt( C[i] ) XOR C[i-1]

The second line is where the attack lives, so it is worth slowing down. To recover a plaintext block on decryption, the receiver runs the ciphertext block C[i] through the cipher’s decrypt function, producing an intermediate value, and then XORs that intermediate value with the previous ciphertext block C[i-1]. Call the intermediate value I[i], so that I[i] = AES_decrypt( C[i] ) and the plaintext is simply P[i] = I[i] XOR C[i-1].

Here is the crucial fact. The intermediate value I[i] depends only on C[i] and the key. It does not depend on C[i-1] at all. If the attacker changes the previous ciphertext block, the cipher still produces the exact same I[i], and the only thing that changes is the XOR applied to it afterward. The attacker controls C[i-1] completely, because it is just data in the ciphertext they are submitting. So the attacker holds one side of the final XOR in their hand. They are one unknown away from the plaintext, and that unknown is I[i].

The leak: one bit that should never escape

Put the two pieces together. The attacker takes a ciphertext block C[i] they want to decrypt, and they prepend a block of bytes they fully control, which the receiver will treat as the previous ciphertext block. The receiver decrypts C[i] to the fixed intermediate I[i], XORs it with the attacker’s chosen block to get some plaintext, and then checks the padding of that plaintext. Because the attacker is choosing the previous block byte by byte, they are choosing the output of that final XOR byte by byte, which means they are steering the plaintext the padding check sees.

The receiver then does the one thing it must not do: it reveals whether the padding was valid. Maybe it returns a BAD_PADDING error distinct from a BAD_MAC error. Maybe both return the same error text but the padding failure comes back a few microseconds sooner because the code bails out before computing a MAC. Maybe a web app returns HTTP 500 on a decryption fault and HTTP 200 on a logic error further down. Any observable difference between valid and invalid padding is the oracle. The attacker does not need the plaintext spelled out. They need the system to answer one yes or no question about ciphertext they crafted, and answer it reliably.

The cipher was never broken. The key never leaked. The system was simply willing to answer, thousands of times, a single question it believed was harmless: did this decrypt to something with valid padding?

The byte at a time decryption

Now the math. The goal is to recover the last byte of the intermediate value I[i], because once every byte of I[i] is known, the real plaintext falls out by XORing I[i] with the genuine previous ciphertext block. Knowing I[i] is knowing the plaintext.

The attacker works on the last byte first. They take their controllable previous block, call it C', and they set its last byte to a guess value g, running g through all 256 possibilities from 0x00 to 0xFF. For each guess they submit C' followed by C[i] to the oracle and watch the answer. The decrypted last plaintext byte that the padding check sees is:

P_last = I_last XOR g

For almost every value of g the padding is invalid and the oracle says no. But there is a value of g for which the last plaintext byte comes out to 0x01, and a final byte of 0x01 is, by itself, valid PKCS#7 padding: it claims a single byte of padding whose value is one. When that happens the oracle says yes. At that moment the attacker knows:

I_last XOR g = 0x01
therefore  I_last = g XOR 0x01

The last byte of the intermediate value is recovered with at most 256 queries, and no key was involved. There is one wrinkle worth naming: occasionally a yes is a false positive, where the byte before the last happened to make the plaintext end in 02 01 or similar, which is also valid. The attacker resolves it by perturbing the second to last byte of C' and re testing; if the padding still validates, the last byte really was forced to 0x01.

Walking right to left across the block

With I_last in hand, the attacker moves to the second to last byte, and the trick is to aim for padding of length two. They want the decrypted block to end in 02 02. They already know I_last, so they can set the last byte of C' to force the final plaintext byte to 0x02 exactly, using C'_last = I_last XOR 0x02. Then they brute force the second to last byte of C' through all 256 values until the oracle reports valid padding, which now means the block ends in the valid two byte pattern 02 02. That reveals the second to last byte of I[i] by the same XOR relation, I_second = g XOR 0x02.

The pattern repeats leftward. To recover the byte at position k, the attacker fixes every already known byte to the right so the tail decrypts to the padding value k_pad repeated, then brute forces position k until the padding validates. Each byte costs at most 256 oracle queries, so a 16 byte block costs at most 16 times 256, roughly 4096 questions, to recover in full. Repeat per block and the entire message is decrypted. The whole thing runs on one fact: P[i] = AES_decrypt(C[i]) XOR C[i-1], with the attacker owning C[i-1] and the oracle confirming when the right side lands on valid padding.

Notice what the attacker never needs. They never see the key, never run the cipher in the forward direction, and never have to guess more than 256 values at any step. The work is linear in the length of the message, not exponential, which is what separates this from brute force and makes it genuinely practical. Picture our invented app, Acme Notes, storing a session as an encrypted cookie and returning a clean error whenever a cookie fails to decrypt into well formed data. An attacker with a stolen cookie they cannot read, but can replay with edits, now has a live oracle: each tweaked cookie comes back valid or invalid, and a few thousand requests later the plaintext session, user id and all, is sitting in front of them. No alarm fires, because every individual request looks like an ordinary client sending a slightly malformed cookie.

Turning the oracle into an encryption machine

The same lever runs in reverse, which surprises people the first time they see it. Once the attacker can recover the intermediate value I[i] for any chosen ciphertext block C[i], they can forge ciphertext that decrypts to any plaintext they want. They pick a plaintext block P_target. They run the padding oracle against an arbitrary C[i] to learn its intermediate I[i]. Then they simply set the previous block to C[i-1] = I[i] XOR P_target, because AES_decrypt(C[i]) XOR C[i-1] = I[i] XOR (I[i] XOR P_target) = P_target. Chaining this construction block by block, working from the last block backward and choosing a fresh C[i] at each step, lets the attacker build an entire ciphertext that decrypts to a message of their choosing, all without the key. A pure decryption oracle has become a forgery tool. Vaudenay’s original paper laid out exactly this reversal.

POODLE and Lucky Thirteen: the oracle in the wild

This is not a chalkboard curiosity. Serge Vaudenay published the attack in 2002 in a paper titled Security Flaws Induced by CBC Padding, applying it to SSL, IPSEC, and WTLS. For years it was treated as a known issue with known mitigations. Then two attacks proved the mitigations were leakier than anyone wanted to admit.

POODLE: CVE-2014-3566

POODLE, disclosed in October 2014 and tracked as CVE-2014-3566, stands for Padding Oracle On Downgraded Legacy Encryption. The flaw lives in SSLv3, an obsolete protocol that almost everything still supported as a fallback. In SSLv3’s CBC mode, the padding bytes are not fully specified and not covered by the message authentication code. The receiver checks the length byte of the padding but does not verify the padding content, which is precisely the validity gap a padding oracle needs. A man in the middle who can force a connection to roll back from TLS to SSLv3, then make the victim resend the same secret over and over across fresh connections, can recover a chosen byte of ciphertext such as a session cookie in around 256 requests per byte. The downgrade is the clever part: even a client and server that both prefer modern TLS can be shoved back onto the vulnerable SSLv3, which is why the fix was not patching SSLv3 but ripping it out entirely.

Lucky Thirteen: the timing variant

Lucky Thirteen, disclosed in 2013 by Nadhem AlFardan and Kenneth Paterson and tracked as CVE-2013-0169, showed that you do not even need an explicit error to build the oracle. TLS implementations had been hardened so that bad padding and bad MAC returned the same error, closing the obvious leak. But the time taken to process a record still depended on the padding, because the amount of data fed into the MAC computation changed with how many bytes the code believed were padding. That tiny timing difference, measured carefully across many sessions, was itself the oracle. The name comes from the 13 byte TLS header that shaped the timing arithmetic. Lucky Thirteen made the point that a side channel does not have to be a message at all. A consistent difference in how long something takes is information, and information about padding validity is a padding oracle.

It is worth placing this alongside its neighbors. A padding oracle is not insecure deserialization, where untrusted bytes become live objects, and it is not a network level fingerprinting trick. But all three share a shape: a component reveals more about how it processed input than it meant to, and an attacker turns that excess into leverage. Here the excess is a single bit about padding, and the leverage is total.

The fix: authenticate before you decrypt

The root cause is that the system makes a decision based on decrypted bytes before it has checked that those bytes are authentic. The padding check runs on ciphertext the attacker forged, and the result of that check escapes. Every fix is a variation on closing that ordering.

The classic construction is encrypt then MAC. After encrypting the plaintext, you compute a message authentication code over the ciphertext, and you append it. On the way back in, you verify the MAC first, over the raw ciphertext, before you decrypt anything or look at any padding. If the MAC does not match, the ciphertext was tampered with, and you reject it immediately, having revealed nothing about padding because you never got that far. The attacker’s forged ciphertext fails the MAC check, the padding check never runs, and there is no oracle to query. The order is the whole point: the authentication has to gate the decryption, not the other way around.

The modern answer folds both jobs into a single primitive: authenticated encryption, most commonly AES-GCM. An AEAD cipher encrypts and authenticates in one operation, so there is no separate padding check to leak and no separate MAC step to misorder. AES-GCM is also a stream style construction that needs no block padding at all, which removes the padding oracle’s target outright. The practical lesson the whole saga taught the field is short: do not compose your own encrypt and authenticate steps, and do not run a plain CBC cipher with a bolt on MAC unless you have proven the ordering and the constant time behavior. Reach for an AEAD mode and let it do both jobs together. The Vaudenay paper that started it all, and the Cryptopals CBC padding oracle challenge that lets you build one by hand, are both worth working through if you want the mechanism in your fingers rather than just your notes.

The assumption that breaks

Strip away the blocks and the XORs and one assumption is left holding the whole thing up. The system assumes that telling the sender whether the padding was valid is harmless. It feels harmless. Padding is plumbing, a formatting detail, the sort of thing you would happily log or return in an error so a developer can debug a malformed request. Surely a yes or no about formatting gives nothing away. But that single bit, asked enough times against ciphertext the attacker controls, is a decryption oracle and a forgery oracle at once. The harmless answer is the entire attack.

The bug is not in AES and not in CBC. It is in a trust boundary drawn one step too late, where a check ran on unauthenticated bytes and its result was allowed to escape. That gap between what a system assumes it is safely revealing and what an attacker can actually reconstruct from it is the kind of flaw you find by asking, of every response a system gives, what does this answer tell someone who is asking it ten thousand times on purpose. It is exactly the kind of assumption an autonomous researcher built to test assumptions is meant to catch: not a known bad string to grep for, but a quiet belief that a side channel was too small to matter. Authenticate before you decrypt, reach for AES-GCM, and treat every difference a system can show, in errors, in status codes, in timing, as something an attacker is already measuring. Learn more about that approach on our about page.

Frequently asked questions

What is a padding oracle attack in simple terms?

It is a way to decrypt CBC encrypted data without the key by abusing a system that reveals whether the padding of a decrypted message was valid. The attacker submits altered ciphertext, watches whether the padding check passes or fails, and uses that single yes or no answer to recover the plaintext one byte at a time. The cipher and the key stay intact; only the surrounding validity check leaks. Serge Vaudenay described the original attack in his 2002 paper Security Flaws Induced by CBC Padding.

How does flipping bytes in the previous ciphertext block recover plaintext?

In CBC mode the plaintext is P[i] = AES_decrypt(C[i]) XOR C[i-1], and the intermediate value AES_decrypt(C[i]) depends only on the key, not on the previous block. Because the attacker fully controls the previous block, they can brute force its last byte through all 256 values until the oracle reports valid padding, which forces the final plaintext byte to 0x01 and reveals the intermediate byte by XOR. Repeating right to left recovers the whole block. The Cryptopals CBC padding oracle challenge walks the math hands on.

What was the POODLE vulnerability?

POODLE, tracked as CVE-2014-3566 and disclosed in October 2014, stands for Padding Oracle On Downgraded Legacy Encryption. It exploits SSLv3, whose CBC padding is not covered by the message authentication code, giving an attacker a padding oracle. A man in the middle forces a connection to roll back from TLS to SSLv3, then recovers a chosen ciphertext byte such as a session cookie in around 256 requests. The fix was to disable SSLv3 entirely, as described in the Oracle POODLE advisory.

How do you prevent a padding oracle attack?

Authenticate before you decrypt. Use encrypt then MAC so the message authentication code is verified over the ciphertext before any padding is checked, which means forged ciphertext is rejected before the padding check ever runs. Better still, use an authenticated encryption mode such as AES-GCM, which combines encryption and authentication in one primitive and needs no block padding to leak. The timing variant Lucky Thirteen showed that even matching error messages leak through timing, so constant time processing matters too.