Learning Goals
3 minBy the end of this lesson you can:
- Recognise the common cryptographic failures that cause real breaches.
- Identify sensitive data that must be protected in transit and at rest.
- Apply the modern defaults: bcrypt, TLS everywhere, Fernet/AES-GCM, secrets in env.
- Audit an app's crypto choices against a checklist.
Warm-Up · The Maths Is Fine; the Usage Isn't
5 minAES, RSA, SHA-256 are not broken. Yet "cryptographic failures" is OWASP #2 — because the failures are in how crypto is applied: using a broken algorithm (MD5 for passwords), not encrypting at all (HTTP), hard-coding keys, or leaking secrets. You've already learned every fix across Lessons 9-17; this lesson assembles them into the A02 picture.
A02 is a category of mistakes, not a single bug: the wrong algorithm, no encryption, leaked or hard-coded keys, weak randomness. The defences are the modern defaults you've practised — and the meta-lesson is "don't roll your own; use vetted libraries with safe settings." First decide what data is sensitive; then protect it in transit (TLS) and at rest (proper hashing/encryption); then protect the keys.
New Concept · The Failures & Their Fixes
14 minThe catalogue of A02 failures
FAILURE FIX (lesson) plaintext transport (HTTP, Telnet, FTP) TLS/HTTPS everywhere (L8-09) passwords as plain/MD5/SHA-256 bcrypt or argon2 + salt (L8-12,13) broken algorithm (MD5, SHA-1, DES, RC4) SHA-256+/AES-GCM/Fernet (L8-11,15) hard-coded / committed keys secrets in env, .gitignore (L8-08,44) weak randomness (random for tokens) secrets module / os.urandom (below) ECB mode / reused IV/nonce authenticated modes, fresh IV (L8-15) no encryption of sensitive data at rest Fernet/AES-GCM (L8-14) sensitive data in logs/URLs/errors redact; never log secrets (L8-45)
Step 1: identify sensitive data
You can't protect what you haven't classified. Passwords, tokens, API keys, personal data (PII), payment info, health records — these need encryption in transit and at rest, and minimal retention. The first A02 question is always "what sensitive data does this app touch, and where does it live?"
Weak randomness — a subtle, common failure
import random, secrets # WRONG: random is predictable — NOT for security tokens, IDs, salts token = "".join(random.choices("abcdef0123456789", k=32)) # ✗ guessable # RIGHT: the secrets module is cryptographically secure token = secrets.token_hex(16) # ✓ 32 hex chars, unpredictable session_id = secrets.token_urlsafe(32) reset_code = secrets.randbelow(1_000_000) # secure random number
random is not for securityPython's random module is a fast pseudo-random generator — predictable if an attacker learns its state. Using it for session tokens, password-reset codes, salts, or anything secret is a real vulnerability. Use the secrets module (or os.urandom) for anything security-sensitive. This one is easy to get wrong and easy to fix.
Putting the fixes together (a secure baseline)
import os, secrets, bcrypt from cryptography.fernet import Fernet # passwords: slow salted hash (never reversible) hashed_pw = bcrypt.hashpw(password.encode(), bcrypt.gensalt()) # tokens / ids: cryptographically secure randomness session_token = secrets.token_urlsafe(32) # data at rest: authenticated encryption, key from the environment key = os.environ["DATA_KEY"] # NOT hard-coded ciphertext = Fernet(key).encrypt(sensitive_bytes) # in transit: enforce HTTPS (config), reject plaintext # (see L8-09: TLS with verification on)
The meta-rules
- Don't roll your own crypto — use
cryptography,bcrypt,secretswith safe defaults. - Encrypt in transit and at rest — both, for sensitive data.
- Protect the keys — env/secret manager, never code; rotate (L8-44).
- Don't leak via the side door — secrets in logs, URLs, or error pages are a crypto failure too.
- Minimise — don't store sensitive data you don't need; you can't leak what you don't have.
Worked Example · Audit & Remediate a Vulnerable Module
12 minGoal: a before/after of a module riddled with A02 failures — the kind of code that ships and then leaks — fixed with the defaults you know.
# BEFORE — a catalogue of cryptographic failures import hashlib, random API_KEY = "sk-live-9f8a7b6c5d4e3f2a1b0c" # ✗ hard-coded secret def store_password(pw): return hashlib.md5(pw.encode()).hexdigest() # ✗ MD5, no salt def make_token(): return str(random.randint(10**9, 10**10)) # ✗ predictable def save_card(num): open("cards.txt", "a").write(num + "\n") # ✗ plaintext at rest def log_login(user, pw): print(f"login {user} pw={pw}") # ✗ secret in logs
# AFTER — modern defaults, every failure fixed import os, secrets, bcrypt, logging from cryptography.fernet import Fernet log = logging.getLogger("auth") API_KEY = os.environ["API_KEY"] # ✓ from env, not code def store_password(pw: str) -> bytes: return bcrypt.hashpw(pw.encode(), bcrypt.gensalt()) # ✓ slow, salted def make_token() -> str: return secrets.token_urlsafe(32) # ✓ cryptographically secure def save_card(num: str) -> None: key = os.environ["CARD_KEY"] # ✓ key from env enc = Fernet(key).encrypt(num.encode()) # ✓ encrypted at rest open("cards.enc", "ab").write(enc + b"\n") # (better still: don't store card numbers at all — use a payment provider) def log_login(user: str, pw: str) -> None: log.info("login attempt user=%s", user) # ✓ never log the password
Audit result: 5 cryptographic failures → 0 hard-coded key → env var MD5 password → bcrypt (salted, slow) random token → secrets.token_urlsafe plaintext card → Fernet-encrypted at rest (or: don't store it) pw in logs → redacted
Read the code
Every fix is a one-liner you've already learned — the value of A02 is the checklist habit of spotting them. Notice the recurring theme: don't invent crypto, use the right library with safe defaults, keep keys in the environment, and don't leak secrets through logs or plaintext storage. The best fix of all is in the comment on save_card: the most secure way to store a card number is to not store it — outsource to a payment provider. Minimising sensitive data is the cheapest A02 defence.
Try It Yourself
13 minGenerate a "reset code" with both random and secrets. Explain, in a comment, why the random one is a vulnerability for a password-reset flow and the secrets one is safe.
Given a code snippet (write your own or use the BEFORE example), list every A02 failure and the specific fix for each. Aim to find at least four distinct categories.
Extend Lesson 25's secret-scanner into an A02 linter that flags: hashlib.md5/sha1 used on passwords, random used for tokens, hard-coded key patterns, and verify=False. Run it on a project and triage the findings.
Hint
import re CHECKS = { "MD5/SHA1 (weak)": re.compile(r"hashlib\.(md5|sha1)\("), "random for security": re.compile(r"random\.(randint|choice|choices|random)\("), "hard-coded secret": re.compile(r"(sk-[A-Za-z0-9]{12,}|api_key\s*=\s*['\"][^'\"]{12,})"), "TLS verify disabled": re.compile(r"verify\s*=\s*False|CERT_NONE"), } def audit(src: str): for name, pat in CHECKS.items(): for m in pat.finditer(src): print(f"⚠️ {name}: {m.group()[:50]}")
Mini-Challenge · A Data-Protection Map
8 minFor an app you built, produce a "data-protection map": list every piece of sensitive data it handles (passwords, tokens, PII, etc.), and for each record how it's protected in transit and at rest — flagging anything unprotected. This is the artifact a real security review starts from.
Show an example map
DATA-PROTECTION MAP — my-blog
data in transit at rest status
passwords TLS (HTTPS) bcrypt hash ✓ ok
session tokens TLS secrets.token_urlsafe ✓ ok
user emails TLS plaintext in SQLite ⚠ acceptable (PII;
encrypt if sensitive)
API key (3rd) TLS env var, not in code ✓ ok
draft posts TLS plaintext ✓ ok (not sensitive)
admin actions TLS logged WITHOUT secrets ✓ ok
Gaps to fix: none critical; consider encrypting emails at rest if
the threat model warrants (e.g. a health/finance app).Non-negotiables: every sensitive data item listed with its in-transit and at-rest protection, and any gaps flagged.
Recap
3 minCryptographic Failures (A02) come from misusing crypto, not from broken maths: plaintext transport, weak algorithms (MD5/SHA-1 for passwords), hard-coded/leaked keys, predictable randomness (random instead of secrets), and leaking secrets via logs/URLs. The fixes are the modern defaults you've practised — bcrypt/argon2, TLS everywhere, Fernet/AES-GCM, secrets in env, the secrets module for tokens — plus the meta-rules: don't roll your own, protect the keys, and minimise the sensitive data you store. Start every review by mapping what's sensitive and how it's protected in transit and at rest.
Vocabulary Card
- cryptographic failure
- Misusing crypto: weak algo, no encryption, leaked key, weak randomness.
- secrets module
- Python's cryptographically secure RNG for tokens/codes (not
random). - in transit / at rest
- Data moving over the network / stored on disk — both need protection.
- data minimisation
- Not storing sensitive data you don't need — the cheapest defence.
Homework
4 minBuild the A02 crypto-failure linter and run it on a real project. Produce a data-protection map for one app you built. Fix at least one genuine A02 issue you find (a weak hash, a random token, a hard-coded key, or missing at-rest encryption). Write a before/after with the failure category and the modern fix for each change.
Sample · A02 remediation
Linter findings on my project (3):
1. [random for security] reset code via random.randint → predictable.
2. [MD5] legacy password check still using hashlib.md5.
3. [hard-coded secret] a Stripe test key in settings.py.
Fixes:
1. random.randint → secrets.token_urlsafe(16). Category: weak
randomness. Why: an attacker who guesses the PRNG state could
predict reset codes and take over accounts.
2. md5 → bcrypt with upgrade-on-login. Category: broken algorithm
/ unsalted. Why: MD5 password hashes are cracked instantly.
3. moved the key to .env + .gitignore, rotated the old key.
Category: hard-coded secret. Why: it was in git history,
readable by anyone with repo access.
Data-protection map shows all sensitive data now: TLS in transit,
bcrypt/Fernet/secrets at rest, keys in env.Non-negotiables: working linter run, a data-protection map, and at least one real A02 fix categorised with its modern remedy.