Cryptographic Failures
Theory
Formerly called Sensitive Data Exposure, this was renamed in 2021 to identify the root cause: weak or missing cryptography. It sits at #4 in 2025 (down from #2 in 2021) because almost every application stores or transmits passwords, tokens, or PII — and the difference between a safe and a catastrophic breach often comes down to a single algorithm choice.
What Goes Wrong
- Passwords hashed with MD5 or SHA-1 — these are general-purpose hashing algorithms, not password hashing functions. They are brutally fast (billions per second on a GPU), making every password in a leaked database crackable within hours
- Base64 used as "encryption" — base64 is reversible encoding, not encryption.
atob("dXNlcjoxMjM=")in any browser returns the original string immediately - Data sent over HTTP — credentials and session tokens travel in plaintext; any network observer captures them
- Hardcoded secrets in source code — API keys and JWT secrets committed to git leak to anyone with repo access
- ECB mode symmetric encryption — identical plaintext blocks produce identical ciphertext, leaking data patterns (see the famous ECB penguin image)
- Weak random number generation — using
random.randint()or a timestamp as a secret/token is predictable
Why MD5 is Broken for Passwords
# MD5 of "admin123" = 0192023a7bbd73250516f069df18b500 # This exact hash is in every publicly available rainbow table. # Speed on a modern GPU: ~10 BILLION MD5 hashes per second. # The entire rockyou.txt wordlist (14 million passwords) takes < 1 second: $ hashcat -m 0 0192023a7bbd73250516f069df18b500 rockyou.txt Cracked: admin123 (0.4 seconds) # Compare bcrypt (cost 12) on the same hardware: # Speed: ~20,000 bcrypt hashes per second # Time to crack same wordlist: ~700 seconds per password (not per list)
Password Hashing Algorithm Comparison
Algorithm GPU Speed Salted? Designed for passwords? ----------- --------------- -------- ----------------------- MD5 10 billion/s No No — general hashing SHA-256 3 billion/s No No — general hashing bcrypt 20,000/s Yes Yes — deliberately slow, adjustable cost Argon2id 1,000/s Yes Yes — memory-hard; current best practice scrypt 10,000/s Yes Yes — memory-hard alternative
Vulnerable Code Pattern
import hashlib, base64, json
# VULNERABLE — MD5 is not a password hashing function
def store_password(password: str) -> str:
return hashlib.md5(password.encode()).hexdigest() # crackable in seconds
# VULNERABLE — base64 is encoding, not encryption
def create_session(user_id: int, role: str) -> str:
data = json.dumps({"user_id": user_id, "role": role})
return base64.b64encode(data.encode()).decode() # trivially reversible
Fixed Code
import bcrypt, secrets
# FIXED — bcrypt with cost factor 12 (~100ms per hash; brute-force infeasible)
def store_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)).decode()
def verify_password(plain: str, hashed: str) -> bool:
return bcrypt.checkpw(plain.encode(), hashed.encode())
# FIXED — cryptographically random 256-bit session token
def create_session() -> str:
token = secrets.token_hex(32) # 32 bytes = 256 bits; stored server-side
session_store[token] = {"user_id": user.id, "role": user.role}
return token # only the opaque token goes in the Set-Cookie header
Real-World Breaches
- RockYou (2009) — 32 million passwords stored in plaintext; became the source of the most-used password cracking wordlist ever
- LinkedIn (2012) — 6.5 million unsalted SHA-1 password hashes leaked; all were cracked within days
- Adobe (2013) — 153 million accounts; passwords encrypted with 3DES in ECB mode. The hints column let researchers decrypt millions without the key
- Ashley Madison (2015) — 36 million accounts; many passwords stored in MD5; cracked and published publicly
How to Fix — Checklist
- Passwords — use bcrypt (cost ≥ 12), Argon2id, or scrypt. Never MD5, SHA-1, or SHA-256 alone
- Session tokens — generate with
secrets.token_hex(32)(Python) orcrypto.randomBytes(32)(Node). The token is an opaque reference; all data lives server-side - Sensitive data at rest — AES-256-GCM with authenticated encryption; store keys separately from data (HSM or secrets manager)
- TLS everywhere — force HTTPS; set
Strict-Transport-Security: max-age=31536000; includeSubDomains - Secrets in config — environment variables or a dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager). Never commit secrets to git
Weak Cookie — Base64-encoded session
The session cookie is base64-encoded JSON — trivially forgeable.
1. Click Submit (alice / alice123) — the result page shows your owasp_session cookie value.
2. Decode it: echo 'PASTE_VALUE' | base64 -d → you'll see {"user_id":2,"role":"user"}.
3. Change user_id to 1 and re-encode: echo -n '{"user_id":1,"role":"user"}' | base64 | tr -d '\n'
4. In DevTools (F12 → Application → Cookies), replace owasp_session with the new value, then navigate to /web/a04/profile.
NEW=$(echo -n '{"user_id":1,"role":"user"}' | base64 | tr -d '\n') && curl -sk -b "owasp_session=$NEW" https://owasp.thexssrat.com/web/a04/profileMD5 Password Hashes Exposed in API
The user listing API returns MD5-hashed passwords. Crack the admin password hash using a rainbow table or hashcat.