Padding Oracle in a Payroll Portal
Northwind Payroll runs a claims portal that stores 12,000 employee records. Each record is encrypted with AES-256-CBC using a fresh 16-byte IV prepended to the ciphertext, and PKCS#7 padding. The service then computes HMAC-SHA1 over the plaintext and appends the 20-byte tag. The same 32-byte key, produced by a single SHA-256 pass over an administrator passphrase, is used for both AES and HMAC. The decryption endpoint returns HTTP 500 'padding invalid' when unpadding fails and HTTP 403 'MAC invalid' when the tag check fails; both are logged with the ciphertext ID. Average request latency is 38 ms against a 40 ms budget, and the crypto operations take about 3 ms. A penetration tester reports that she can decrypt a captured record without the key after roughly 4,000 requests to the endpoint. Management wants a fix that ships this sprint and does not require re-encrypting the archive.
1. Name the attack class the tester exploited, explain the specific design choices that enabled it, and state the one architectural change that removes it for new data. What can be done for the existing archive without re-encrypting it?
Read the worked answer
The tester is using a CBC padding oracle. The service is MAC-then-encrypt: it decrypts, unpads, and only then checks HMAC-SHA1 over the plaintext. Because unpadding failures and MAC failures return different status codes, each request leaks information about the intermediate plaintext block. Roughly 128 guesses per byte are enough on average, so 4,000 requests to recover a 32-byte record is plausible.
The fix for new data is encrypt-then-MAC: compute HMAC-SHA-256 over IV || ciphertext, verify the tag in constant time before any decryption, and return a single generic error for all failures. That makes padding errors indistinguishable from MAC errors.
The existing archive is harder because the tag covers plaintext, so the service cannot verify before decrypting. Immediate containment is to return identical errors and constant-time responses, remove the distinct log fields exposed to callers, and rate-limit the endpoint; then re-wrap records in the background or on next write. Do not claim the archive is fixed until it is re-encrypted or wrapped under an authenticated scheme.
2. The CISO proposes replacing HMAC-SHA1 with HMAC-SHA256 but keeping the single SHA-256 passphrase hash. Evaluate both parts. Is HMAC-SHA1 already forgeable, and what KDF parameters would you set?
Read the worked answer
HMAC-SHA1 is not practically forgeable today; SHA-1 collision attacks do not translate into HMAC key recovery or forgery because HMAC does not use the hash in a collision-sensitive way. Replacing it is still right for deprecation and policy, but it is not the urgent break.
The urgent problem is the key derivation: a single SHA-256 pass over a human passphrase is not a password KDF. An attacker who steals the database can test guesses at GPU speed, and because the same 32 bytes feed AES and HMAC, compromise of one path weakens the other. Use Argon2id with a per-installation salt, 64 MiB memory, t=3, p=1, or PBKDF2-HMAC-SHA256 at 600,000 iterations if Argon2 is unavailable. Then run HKDF-SHA-256 with distinct info strings to derive separate AES and MAC keys. Do the expensive KDF once at startup or unlock, not per request. Also rotate the passphrase-derived key and use a high-entropy random master key wrapped by the KDF.
3. The 40 ms budget allows only about 2 ms of new work. Which parts of your recommendation affect per-request latency, and which can be moved off the request path? Justify the split.
Read the worked answer
Per request, HMAC-SHA-256 over ciphertext is linear and small, and a constant-time tag comparison is negligible at these sizes. Encrypt-then-MAC adds one hash pass over the ciphertext; at typical record sizes this is well under a millisecond. The KDF is the dangerous part: Argon2id at 64 MiB is deliberately tens to hundreds of milliseconds and must not run on every request. Derive and cache the master key at startup or at first unlock, keep it in memory with strict access control, and use HKDF per-record or per-session only if needed.
Re-encrypting the 12,000-record archive is an offline batch job; it does not belong in the 40 ms path. Rate limiting and generic error handling add little CPU. The design should measure p99, not average, because the oracle fix depends on uniform timing. If the archive cannot be re-encrypted this sprint, isolate the legacy read path behind a separate service with strict rate limits and monitoring.