In 2009 two researchers showed they could add parameters to a signed Flickr API request and produce a valid signature for the modified version, without ever recovering the shared secret. Nothing was wrong with MD5's collision resistance that day. The problem was in the construction underneath it, and SHA-256 is built the same way.
A developer who needs to prove a request came from someone holding a shared key reaches for the tool that is already in the standard library. Concatenate the key and the message, hash the result, send the digest along. The server repeats the computation and compares. It looks like a signature, it is one line of code, and against MD5, SHA-1, SHA-256 and SHA-512 it is forgeable by anyone who has seen a single valid pair.
The reason has nothing to do with how hard it is to find two inputs with the same digest. It is a consequence of what the digest of these functions actually is.
The digest is a snapshot the attacker can resume
MD5, SHA-1 and the SHA-2 family are built on the Merkle-Damgard construction. The input is padded to a multiple of the block size, 512 bits for SHA-256. An internal state, eight 32-bit words for SHA-256, starts at a fixed initialisation vector. A compression function mixes each block into that state in turn. When the blocks run out, the function returns the state.
That last step is where the trouble lives. There is no finalisation, no separate squeeze, no discarded capacity. The 256 bits you receive are the complete working state of the algorithm at the moment it stopped. Hand those bits back to the compression function and it will happily carry on from exactly where it left off, on data it has never seen.
The padding is public and deterministic. For SHA-256 it is a single 1 bit, written as the byte 0x80, then as many zero bytes as needed, then the length of the message in bits as a 64-bit big-endian integer. An attacker who knows how long the secret is can write out that padding themselves.
What the attacker gets to do
Suppose a server computes tag = SHA-256(secret || message) and publishes both message and tag. An attacker who wants the tag for a longer message proceeds like this:
- Guess the byte length of the secret. Most are short, so a few dozen guesses covers the realistic range.
- Construct the glue padding that SHA-256 would have appended to
secret || message. - Load
taginto a fresh SHA-256 context as its starting state, and set the length counter to the number of bits already consumed. - Hash the desired extension bytes and read out the new digest.
The result is a valid tag for secret || message || glue padding || extension. The attacker never learns the secret and never needs to. They have forged a tag for a message they chose the tail of.
Why the junk bytes rarely help you. The forged message carries the glue padding in the middle of it, which looks like an obvious tell. In practice it usually is not. Query-string parsers that take the last occurrence of a repeated key, formats that ignore trailing bytes, and length-prefixed records that skip to an offset all absorb the padding without complaint. The forgery only fails if the parser is stricter than the MAC.
None of this requires research-grade tooling. Ron Bowes published hash_extender over a decade ago, which takes the original message, the known tag, the extension and a guessed secret length, and prints the forged pair for every common Merkle-Damgard hash.
Flickr, 2009
The best-documented production case is the Flickr API signature forgery published by Thai Duong and Juliano Rizzo in September 2009. Flickr signed API calls by sorting the request parameters, concatenating them onto a shared secret, and taking the MD5 of the result. Every ingredient of the attack was present: a secret at the front, attacker-visible message content, and a published digest.
The write-up made a broader point that has aged well. The developers had not chosen MD5 carelessly; the same call would have been just as forgeable with SHA-256. Upgrading the hash function does nothing here, because the property being abused is one that the stronger hash also has.
This is the same category of mistake as the cryptographic doom principle: a scheme assembled from sound primitives, composed in an order that voids their guarantees.
Which hash functions are affected
| Function | Extendable? | Why |
|---|---|---|
| MD5, SHA-1 | Yes | Merkle-Damgard, full state published. Both are also broken for collisions. |
| SHA-256, SHA-512 | Yes | Collision-resistant and still fully extendable. Strength does not remove the property. |
| SHA-224 | Partly | Truncates a 256-bit state to 224 bits, so 32 bits of state are missing. That is a small gap, not a defence. |
| SHA-384, SHA-512/256 | No | A large part of the internal state is discarded before output, so it cannot be reconstructed. |
| SHA-3 / Keccak | No | A sponge construction; the capacity portion of the state is never emitted. |
| BLAKE2, BLAKE3 | No | The final block is processed with a finalisation flag set, so a resumed state produces different output. |
The distinction that matters is whether the function throws anything away before it hands you a digest. SHA-3's designers chose a sponge partly for this reason: the capacity bits absorb the entropy an attacker would need to continue, and are never revealed. That is why prefixing a key to a SHA-3 hash is actually safe, and why NIST specified KMAC in SP 800-185 as a direct keyed mode rather than wrapping SHA-3 in something else.
HMAC is shaped by this exact problem
HMAC, specified in RFC 2104 in 1997, looks strange at first reading. It hashes twice, with two derived keys:
HMAC(K, m) = H( (K XOR opad) || H( (K XOR ipad) || m ) )
The inner hash is extendable, and it does not matter. Its output feeds a second hash whose input is a fixed length: one key block plus one digest. An attacker who appends bytes to m changes the inner digest to a value they cannot predict without the key, and the outer hash they would need to recompute is keyed too. There is nothing to resume.
The construction dates from the same period as the attack it defends against, which is worth remembering when reading it. HMAC is not overengineered. Every part of it is answering something specific.
What to use instead
For authenticating a message with a shared symmetric key, in rough order of how often each fits:
- HMAC-SHA-256. Available everywhere, well understood, and correct with any Merkle-Damgard hash underneath it.
- An AEAD instead of a bare MAC. If the payload should also be confidential, AES-GCM or ChaCha20-Poly1305 authenticate and encrypt in one construction, and neither leaves the composition order to the caller.
- KMAC when the codebase is already on SHA-3, or BLAKE3 in keyed mode when throughput matters.
- A signature scheme such as Ed25519 when the verifier should not be able to produce tags of its own. A shared MAC key means every verifier is also a forger.
Two operational details travel with the choice. Compare tags in constant time, since a byte-by-byte comparison that returns early leaks the correct value one position at a time to anyone who can measure it; see constant-time programming for why the obvious implementation is the wrong one. And include everything that matters inside the authenticated bytes, with unambiguous separators. A MAC over concatenated fields with no delimiter authenticates the concatenation, not the fields, and two different parameter sets that concatenate to the same string will share a tag.
The general lesson
Length extension is a useful case to keep in mind because it breaks an intuition rather than an algorithm. The intuition is that a cryptographic hash behaves like a sealed one-way box, so anything built out of one inherits its strength. Merkle-Damgard hashes are one-way with respect to their input and completely transparent with respect to their state, and a homemade MAC sits precisely on the second property.
The practical rule is narrow enough to remember. If a design calls for proving that a message came from a key holder, use a primitive whose name contains the word MAC. Hash functions authenticate nothing on their own, whatever you concatenate to the front of them.
Originally published at havenmessenger.com
Top comments (0)