EECC study materials
EECC study resources

Three Audit Findings in Password Storage and API Signing

EECC · Put your knowledge to work in context.

All case studies

Three Audit Findings in Password Storage and API Signing

An audit of a B2B SaaS platform with 2.4 million accounts and 61,000 active API clients produced three findings. Passwords for accounts created before 2016 are stored as unsalted SHA-1, 40 hex characters, in the same table as newer bcrypt cost-12 hashes. The partner API authenticates each request with a header containing an HMAC-SHA1 over the request body and a timestamp, truncated to 96 bits and compared with ordinary string equality. The webhook scheme is different: the sender computes SHA-256 over the concatenation of the shared secret and the JSON payload and sends the digest in a header. A penetration test showed that one webhook receiver could be tricked into accepting a modified payload. The largest API partner says it cannot change its client library for nine months. There is no password-reset enforcement, and marketing objects to a forced reset.

1. Which finding is genuinely exploitable, and why does the HMAC truncation not create the same problem?

Read the worked answer

The webhook finding is the exploitable one, and the cause is the construction, not the hash function.

SHA-256 is a Merkle-Damgard construction: it processes the message in blocks and the output is the internal state. If the verifier computes SHA-256(secret || body), an attacker who knows the digest, the secret length, and the body can compute the padding that was applied, append chosen bytes, and produce a valid digest for secret || body || padding || extra without knowing the secret. The receiver accepts the modified body. JSON parsers that tolerate duplicate or trailing keys make it worse, because an added field can change behaviour even when the original fields are untouched. Fix it by switching to HMAC-SHA256 over body plus timestamp plus nonce, and by rejecting duplicate keys.

HMAC truncation is not the problem. HMAC is not length-extendable: the key is padded and hashed twice, so published output does not expose a continuable state. Truncating to 96 bits leaves a forgery probability of 2^-96 per attempt, far below anything reachable through a rate limit, and NIST permits truncation down to half the hash length. SHA-1's collision weakness is irrelevant to HMAC-SHA1, which does not depend on collision resistance in that way. Moving to HMAC-SHA256 is still worth doing, on hygiene and library-support grounds.

Three secondary defects in that header are worth fixing in the same change: ordinary string comparison leaks a timing signal, so use a constant-time compare; the signature does not cover the HTTP method and path, so a valid signature can be replayed against another endpoint that accepts the same body; and the timestamp needs a bounded window with a nonce cache to stop replay.

2. Design the password migration that avoids forcing a reset for 2.4 million accounts, and state what it does and does not achieve.

Read the worked answer

Start from the threat model: unsalted SHA-1 is an offline-cracking problem, not a collision problem. A GPU performs billions of SHA-1 evaluations per second and precomputed tables cover common passwords, so anyone who obtains a database read recovers a large fraction of those passwords quickly. The exposure is retrospective; you cannot undo a copy that has already left. The goal is to make future dumps expensive and to shrink the population protected only by SHA-1.

Two mechanisms. For active users, opportunistic rehash: keep the legacy verifier in the login path and, on each successful authentication, compute Argon2id (or bcrypt cost 12) over the plaintext, store it with an algorithm tag, and clear the legacy digest. Conversion of 60 to 80 percent of active users within 90 days is typical.

For dormant users you never see plaintext, so wrap: store bcrypt over the SHA-1 hex digest and keep the original verifier available only to the login path. A stolen dump then costs one bcrypt evaluation per guess instead of one SHA-1, moving an attacker from billions of guesses per second to thousands. That is not as good as Argon2id on plaintext, but it is a two-order-of-magnitude improvement for accounts nobody can migrate yet. Re-wrap to Argon2id on next login.

Carve-outs: administrators, anyone with payment or data-export roles, and any account whose password appears in a breach corpus should be forced to reset with sessions invalidated, whatever marketing says. Add a pepper held in an HSM or KMS outside the database so a dump alone is insufficient, and tighten database read and backup handling.

What it does not achieve: it does not protect users who reused the same password elsewhere, and it does not undo any copy already taken. That is the argument for breach notification planning, not for delaying the migration.

3. How do you handle the API partner that needs nine months, and what do you do in the interim?

Read the worked answer

Treat nine months as a negotiation about a date, not about whether the change happens.

Publish a versioned signing scheme with HMAC-SHA256 over method, path, body, timestamp, and a nonce. Keep v1 verification running during the overlap, issue the partner a v2 secret, and instrument per-client v1 usage so the cutover is backed by numbers rather than assurances.

Set dated milestones: library change in the partner's staging environment at 60 days, production at 150 days, v1 disabled at 240 days. Put the date in a contract amendment and attach a rate limit plus an alert threshold on v1 traffic so drift is visible early.

For the interim, reduce the value of a forged or replayed request with controls that need no partner code change: IP allowlisting if their egress is stable, per-client rate limits, a five-minute timestamp window with a replay cache, request size limits, and alerting on unusually large or unusually shaped requests. None of these fixes the construction, so they belong in a written risk acceptance with an expiry date.

Your leverage is that the partner's endpoint accepts the webhook, so a forged payload is their fraud and their notification obligation as much as yours. Say that plainly and early, and offer to test their v2 implementation with them - a partner who knows the incident lands on their side of the boundary usually finds the engineering time.