The Hashes Without Salt — Python Bug Hunt
Modelled on the LinkedIn password leak of June 2012. About 6.5 million LinkedIn password hashes were posted online.
- Language: Python
- Layer: Database
- Difficulty: Medium
- Concepts: Security, Hashing
- Modelled on: LinkedIn · 2012
- Visible tests: a stored password verifies and a wrong one does not; two members with the same password get different hashes
- Reward: 50 XP for a complete fix
Briefing
Modelled on the LinkedIn password leak of June 2012. About 6.5 million LinkedIn password hashes were posted online. They were unsalted SHA-1: the same password always produced the same hash, so one precomputed guess cracked every account that shared it, and a fast hash made guessing cheap. LinkedIn confirmed the leak and moved to salted hashing; years later a far larger dump from the same breach surfaced.
passwords.py stores and checks member passwords. The storage policy in config.py has long said what it should be; the code never followed it.
Rewrite hash_password and verify_password to follow the policy.
Bug report
BUG-LI-2012 · Priority: Critical · Reported by: security
hash_password(password, rng) returns "pbkdf2_sha256$<iterations>$<salt hex>$<hash hex>"
- salt = rng(config.SALT_BYTES) — a fresh salt per call, from the injected rng
- hash = hashlib.pbkdf2_hmac("sha256", password UTF-8, salt, config.ITERATIONS, config.KEY_BYTES)
- hex is lowercase
verify_password(password, stored):
- recomputes with the stored salt and iteration count, compares in constant time (hmac.compare_digest), returns True/False
- anything not in the four-part format above (including a legacy bare SHA-1 hex digest) returns False — never raises
Observed: two members with the password "linkedin" have byte-identical stored hashes.
Logs
[audit] users.password_hash: 6.5M rows, 0 salts
[audit] top hash 7c4a8d09ca3762af61e59520943dc26494f8941b shared by many accountsThe code as shipped
src/accounts/passwords.py (editable)
import hashlib
import hmac
config = bug_require("./config.py")
def hash_password(password, rng):
return hashlib.sha1(password.encode("utf-8")).hexdigest()
def verify_password(password, stored):
candidate = hashlib.sha1(password.encode("utf-8")).hexdigest()
return hmac.compare_digest(candidate, stored)
Read-only context: src/accounts/config.py.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More Python bug hunts.