Module 12 · Production Engineering · Drills

Drills: Security Essentials

Security knowledge that lives only in your head fails under pressure. Type each fix yourself, run it, then reveal the solution. These are the exact tasks you'll be handed in a take-home or a whiteboard.

How to use this page Each drill is a small, real hardening task. Attempt it first, then click “Show solution” to compare. If yours is correct but different — good, that's fluency. Tick each box as you go; your progress is saved in this browser.

A · Warm-up reps Basic

Drill 1 hashing

Hash the password "hunter2-correct-horse" with argon2, then verify a correct and an incorrect attempt against the stored hash.

Show solution
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher()
stored = ph.hash("hunter2-correct-horse")   # salt is automatic

print(ph.verify(stored, "hunter2-correct-horse"))  # True
try:
    ph.verify(stored, "wrong")
except VerifyMismatchError:
    print("rejected")                          # rejected

Never store the plain password. The hash string already contains the algorithm, salt, and cost parameters — that's all you keep.

Drill 2 sql injection

This query is vulnerable. Spot why, then rewrite it with a parameterized query.

email = request.query["email"]
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
Show solution
# The f-string lets input like  ' OR '1'='1  become SQL.
# Fix: pass the value SEPARATELY as a parameter.
cursor.execute(
    "SELECT * FROM users WHERE email = ?", (email,)
)

# Or with the ORM (parameterized for you):
user = session.query(User).filter(User.email == email).first()

The driver treats the placeholder value as data, never as part of the SQL — so input can't change the query's structure.

Drill 3 cors

Configure FastAPI CORS so only https://docchat.example.com may call the API with credentials. Avoid the unsafe wildcard.

Show solution
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://docchat.example.com"],  # exact origin
    allow_credentials=True,
    allow_methods=["GET", "POST"],
    allow_headers=["Authorization", "Content-Type"],
)

allow_origins=["*"] with allow_credentials=True is forbidden by browsers — it would let any website act as your logged-in users.

B · Stretch Intermediate

Drill 4 rate limit

Add a slowapi rate limit of 5 requests per minute per IP to the /ask endpoint.

Show solution
from slowapi import Limiter
from slowapi.util import get_remote_address
from fastapi import Request

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter

@app.post("/ask")
@limiter.limit("5/minute")
def ask(request: Request, q: Question, user=Depends(current_user)):
    ...   # the Request param is required by slowapi

Rate limiting protects the expensive LLM call from abuse and keeps your bill predictable — an "insecure design" fix (OWASP A04).

Drill 5 authorization

Write an ownership check for GET /documents/{doc_id}: the caller may only read their own document. Return 404 if it's missing or not theirs.

Show solution
@app.get("/documents/{doc_id}")
def get_document(doc_id: int, user=Depends(current_user)):
    doc = db.get_document(doc_id)
    if doc is None or doc.owner_id != user.id:
        raise HTTPException(status_code=404, detail="Not found")
    return doc

Returning 404 instead of 403 hides whether the id even exists, so an attacker can't enumerate other users' documents. Fixing exactly the IDOR from the lesson.

C · Build challenge Build

Mini-project · security review Run a security review on the DocChat snippet below. Find 5 distinct issues, name the OWASP-ish category for each, and write the fix. This is exactly what a senior dev does in a code review — and what a take-home grades you on.

Build · review & fix

The code under review:

# settings
JWT_SECRET = "supersecret123"   # hardcoded

@app.post("/signup")
def signup(email, password):
    db.execute(f"INSERT INTO users VALUES ('{email}', '{password}')")

@app.get("/documents/{doc_id}")
def get_doc(doc_id, user=Depends(current_user)):
    return db.get_document(doc_id)

app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True)
Show solution
# 1 Crypto failure: secret hardcoded & in source.
#   -> load from env via Settings; rotate it; .env in .gitignore.

# 2 Crypto failure: password stored in plain text.
#   -> hash with argon2 before insert: ph.hash(password)

# 3 Injection: f-string SQL on signup.
#   -> parameterize: db.execute("INSERT ... VALUES (?, ?)", (email, pw_hash))

# 4 Broken access control (IDOR): no ownership check on get_doc.
#   -> if doc is None or doc.owner_id != user.id: raise 404

# 5 Security misconfig: CORS "*" with credentials.
#   -> allow_origins=[settings.frontend_origin]

# Hardened version:
@app.post("/signup")
def signup(body: SignUp):
    pw_hash = ph.hash(body.password)
    db.execute("INSERT INTO users (email, pw_hash) VALUES (?, ?)",
               (body.email, pw_hash))

@app.get("/documents/{doc_id}")
def get_doc(doc_id: int, user=Depends(current_user)):
    doc = db.get_document(doc_id)
    if doc is None or doc.owner_id != user.id:
        raise HTTPException(status_code=404, detail="Not found")
    return doc

Bonus issues if you spotted them: no rate limit on signup (brute-force / spam), no Pydantic validation on the inputs, and no length bound on the password. Naming extra issues is what separates a mid from a senior in review.

D · Rapid recall Flashcards

Click a card to flip it. Say the answer out loud before you flip — these are the ones interviewers ask verbatim.

How do you store passwords?
A slow, salted hash — argon2 or bcrypt. Never plain text, never md5/sha1.
click to flip
Where do you keep a JWT in the browser?
An httpOnly, Secure, SameSite cookie — not localStorage (XSS can read it).
click to flip
What is an IDOR?
Insecure Direct Object Reference — reading another user's object by guessing its id. Fix: object-level ownership check.
click to flip
What stops SQL injection?
Parameterized queries / an ORM — values travel separately from the SQL text.
click to flip
Why is React XSS-safe by default?
It escapes {values} as text. The trap is dangerouslySetInnerHTML — sanitize first.
click to flip
Scan dependencies for known CVEs?
pip-audit for Python, npm audit for the front-end. Wire into CI.
click to flip

E · Self-check before moving on

Tick each only if you can do it without looking:

Next Secure foundations in place — now make it fast and scalable. Next we cover caching and background jobs: Lesson 12.2 — Caching & Background Jobs.