After this lesson, you will be able to: Read another developer's code (JavaScript, Python, or SQL) and find vulnerabilities by pattern recognition before running the code.
Code review is the highest-leverage AppSec skill: one good reviewer prevents bugs from ever reaching production. This lesson teaches the canonical insecure patterns to grep for and the discipline that turns reviewers into engineers.
When you read code, ask: where does untrusted data enter? where does it leave? what transformations happen along the way? Most vulnerabilities live at the boundary, request input concatenated into a query, response output not encoded, file paths assembled from user-controlled strings. Train yourself to slow down at the boundaries.
Each of these is a real bug from a real codebase.
// 1. SQL Injection via string concatconst q = `SELECT * FROM users WHERE id = ${req.query.id}`; // BAD// Fix: parameterised queryconst q = `SELECT * FROM users WHERE id = $1`; pool.query(q, [req.query.id]);// 2. Command Injection via shellrequire('child_process').exec(`ping ${req.body.host}`); // BAD// Fix: execFile + array args, no shellrequire('child_process').execFile('ping', ['-c', '1', req.body.host]);// 3. SSRFfetch(req.body.url); // BAD// Fix: allowlist hostnames; reject 169.254.169.254, 127.0.0.1, *.internal// 4. Path traversalfs.readFileSync(`./uploads/${req.params.name}`); // BAD (../../etc/passwd)// Fix: path.resolve and check it stays inside ./uploads/
Read these and predict the bug before reading the fix.
# 1. Pickle deserialization of untrusted dataimport pickleuser_obj = pickle.loads(request.body) # BAD, arbitrary code execution# Fix: never pickle untrusted input. Use JSON or a typed schema.# 2. eval / exec on user inputresult = eval(request.args['expr']) # BAD# Fix: parse with ast.literal_eval for limited literals, or build a real parser# 3. SSTI (Server-Side Template Injection) in Jinja2template = f"Hello, {request.args['name']}" # BAD when rendered# Fix: Jinja2 autoescape + never interpolate user input into template source# 4. YAML.load (full loader) on untrusted inputimport yaml; yaml.load(open('config.yml')) # BAD# Fix: yaml.safe_load(...)
1. Auth and authz on every endpoint? (Is the middleware actually applied?) 2. Input validation at the boundary? (Schema validation library, e.g., zod, pydantic.) 3. Output encoding context-aware? (HTML vs JSON vs SQL.) 4. Secrets in code or env? (Grep for AWS_, _TOKEN, _KEY, etc.) 5. Crypto primitives correct? (No MD5/SHA-1 for passwords, no ECB mode, no hardcoded IVs.) 6. Error messages safe? (No stack traces / DB structure leaking to users.) 7. Dependencies pinned and scanned? (npm audit, pip-audit, dependabot.) 8. Logging and observability? (Audit trail for sensitive actions.)
Find an open PR on a real GitHub project that touches request handling. Read it like a reviewer.
Find an open PR in an active repo (e.g., browse popular Python or Node.js projects on GitHub)
Pick a PR that touches user input, file handling, or external API calls
Apply the 30-minute checklist above
Write a review comment for each concern you find. Be specific and helpful, not nitpicky
Compare your findings against the actual PR review by maintainers
Reviewing the diff in isolation, missing how the new code interacts with existing trust boundaries. Always read the calling code too. Approving 'it compiles' without testing the security claim (e.g., the prepared statement is parameterised correctly but the schema migration changed the column type from int to text, which re-opens injection through casting). Blocking on style while letting real vulnerabilities through. Prioritise. Trusting comments that say 'sanitised'; check the sanitisation function actually exists and does what the comment claims.
Pick the cleanest one-line answer.
Sign in and purchase access to unlock this lesson.