Roadmap
Secure Code Reviewer
The specialist who reads application source code from an attacker's perspective. Identifies security vulnerabilities, business logic flaws, and design-level weaknesses that automated tools miss, then produces findings that developers can act on. Often a focused function within AppSec rather than a standalone title at smaller organizations.
OPTIMISTIC 2-3 years · REALISTIC 3-4 years
Stage 00
Programming Fundamentals
Code review requires reading code fluently. This is the prerequisite that makes everything else possible.
Required Programming Depth (all from AppSec path Stage 0)
- Python — deep fluency; reading libraries, async patterns, security-relevant APIs
- JavaScript / TypeScript — frontend and Node.js; framework patterns; async/await
- Java — object-oriented patterns; Spring; annotations; exception handling
- Additional reading ability: Go, C/C++, PHP, Ruby
Reading Code vs Writing Code
- Reviewers read more than they write; develop speed-reading habits for code
- Pattern recognition — recognizing vulnerability patterns before fully understanding context
- Top-down and bottom-up reading — starting from entry points vs starting from sinks
- Skimming for signals — quickly identifying security-relevant code sections
- Data flow tracing — following untrusted input from source to sink through the call stack
Version Control Basics
- Git — reading diffs; understanding what changed between commits
- Pull request review workflow — GitHub/GitLab PR review, inline comments, request changes
- Branch models — feature branches, main/master, release branches
- Code history — git blame, git log — understanding why code is the way it is
Resources
- All from AppSec Stage 0. Same programming resources apply.
Stage 01
Web and Application Security
All vulnerability knowledge from AppSec path Stage 1 applies here — OWASP Top 10 at code level, HTTP/TLS/auth protocols, and API security.
Complete Web Security Knowledge Required
- OWASP Top 10 — code-level recognition in each primary language
- Authentication and session management — JWT, OAuth, session tokens
- API security — REST, GraphQL, OWASP API Top 10
- Cryptography — correct and incorrect implementations
- OWASP ASVS — use as a comprehensive code review checklist
OWASP Code Review Guide
- The canonical reference for secure code review; read in full
- Part 1: Why and how to perform code reviews
- Part 2: Vulnerability types and patterns to identify
- Taint analysis — tracking untrusted data from source to sink
- Specification-based review — comparing code behavior against security requirements
- Risk-based review — prioritizing high-risk code sections first
- Attack surface enumeration — identifying all external input points
Resources
- OWASP Code Review Guide v2 (free at owasp.org)
- PortSwigger Web Security Academy (free)
- OWASP ASVS (free)
Stage 02
Security Fundamentals
Same as AppSec path — CIA Triad, threat modeling awareness, cryptography, network security, authentication protocols.
Certification
- CompTIA Security+
Stage 03
Manual Code Review Methodology
The systematic process that separates reviewers who find critical vulnerabilities from those who only find what automated tools already caught.
Review Preparation
- Understand the application before reading code: what does this application do, what are trust boundaries, what data is sensitive, what is the threat model
- Gather context: architecture documentation, previous pen test reports, security requirements, developer-provided information
- Define scope: full codebase audit vs targeted feature review vs diff review (PR); estimated time allocation based on scope and risk
Entry Point Analysis (Top-Down)
- Start from all external entry points: HTTP endpoints, API routes, form handlers, file uploads, WebSocket handlers, scheduled jobs, message queue consumers
- For each entry point ask: what data comes in, is input validated on server side, where does input go next, does it reach a security-sensitive sink
Sink Analysis (Bottom-Up)
- SQL execution: `connection.execute()`, `db.query()`, `Statement.executeQuery()`
- OS commands: `subprocess.call()`, process spawn APIs, `Runtime.exec()`
- File operations: `open()`, `readFile()`, `FileInputStream()`
- HTML rendering: `innerHTML =`, `document.write()`, `dangerouslySetInnerHTML`
- Deserialization: `pickle.loads()`, `ObjectInputStream.readObject()`, `JSON.parse()` with eval
- Redirects: `redirect()`, `response.sendRedirect()`, `Location:` header construction
- SSRF-prone: `requests.get(url)`, `fetch(url)`, `HttpClient.get(url)` with user-controlled URL
- For each sink: trace backward to the origin; is user input reaching this sink without sanitization?
Taint Analysis
- HTTP request parameters — `request.GET['id']`, `req.query.id`, `request.getParameter("id")`
- HTTP request body — JSON body, form POST data, file upload content
- HTTP headers — User-Agent, X-Forwarded-For, Cookie, custom headers
- URL path parameters — `/users/{id}`, `/api/{resource}`
- Database values (if database is populated from untrusted input — second-order injection)
- External API responses — if treated as trusted when they should not be
- Environment variables (in some contexts)
- Input validation — allowlist validation that rejects unexpected input
- Output encoding — HTML encoding, URL encoding, SQL parameterization
- Parameterized queries — breaks SQL injection taint chain
- Escaping functions — escapeHtml(), htmlspecialchars() — verify correct usage
Specification-Based Review
- Authentication requirement: find where auth gates are enforced; is it enforced on every path?
- Authorization requirement: find every endpoint; check ownership validation
- Cryptography requirement: find every place passwords are stored; verify bcrypt (not SHA-256 or MD5) is used
- Audit requirement: find all admin endpoints; verify logging is present
Python Vulnerability Patterns
- SQL injection — f-string and concat patterns vulnerable; parameterized `cursor.execute("... WHERE id=%s", (user_id,))` safe
- Command injection — `subprocess.call(f"ping {host}", shell=True)` and `os.system("ls " + directory)` vulnerable; `subprocess.call(["ping", host])` without shell=True safe
- Pickle deserialization — `pickle.loads(data)` on user data vulnerable; use JSON instead, HMAC sign if pickle required
- SSTI — `Template(user_input).render()` Jinja2 from user input vulnerable; static template with parameterized render safe
- Path traversal — `open(base_dir + filename)` vulnerable; `os.path.join(base_dir, os.path.basename(filename))` or pathlib.resolve() safe
JavaScript / Node.js Vulnerability Patterns
- SQL injection — concat and `knex.raw` with concat vulnerable; parameterized `db.query("... WHERE email=?", [email])` safe
- Prototype pollution — `merge(target, userInput)` vulnerable (user can set `__proto__.isAdmin`); lodash.merge, jQuery.extend(true), custom recursive merge; test whether `__proto__.privilege` affects privilege checks elsewhere
- XSS — `element.innerHTML = userInput` and React `dangerouslySetInnerHTML={{ __html: userInput }}` vulnerable; `element.textContent = userInput` safe (text node, not HTML)
- Path traversal (Express) — `res.sendFile(req.query.file)` vulnerable; allowlist + `path.resolve()` with base directory check safe
- RegExp DoS (ReDoS) — `new RegExp(userInput)` vulnerable; complex regexes with backreferences on user input can cause catastrophic backtracking
Java Vulnerability Patterns
- SQL injection — `Statement.execute("... WHERE id=" + userId)` vulnerable; `PreparedStatement.setString(1, userId)` safe
- XML External Entity (XXE) — default DocumentBuilderFactory config vulnerable; `factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)` safe
- Deserialization — `ObjectInputStream.readObject()` on untrusted data vulnerable; allowlist via `ObjectInputFilter`; avoid deserializing untrusted data
- Spring Mass Assignment — `@RequestBody User user` binds all JSON fields including `isAdmin`; use DTOs with only allowed fields and `@JsonIgnoreProperties` on sensitive fields
- Path traversal — `new File(baseDir + "/" + filename)` vulnerable; `Path.resolve().normalize()` with `startsWith(basePath)` check safe
Go Vulnerability Patterns
- SQL injection — `db.Query("... WHERE id=" + id)` vulnerable; `db.Query("... WHERE id=$1", id)` safe
- Command injection — `exec.Command("sh", "-c", "ping "+host).Run()` vulnerable; `exec.Command("ping", host).Run()` safe (argv slice, no shell)
- Open redirect — `http.Redirect(w, r, r.URL.Query().Get("next"), 302)` vulnerable; validate target URL against allowlist or same-origin
PHP Vulnerability Patterns
- SQL injection — `mysqli_query($conn, "... WHERE id=$_GET['id']")` vulnerable; PDO with prepared statements safe
- File inclusion — `include($_GET['page'] . '.php')` local/remote file inclusion vulnerable; allowlist of permitted pages, never user input directly in include/require
- Command injection — `system("curl " . $_GET['url'])` vulnerable; `escapeshellarg()` partial mitigation, but avoiding shell execution with user input is safer
- Deserialization — `unserialize($_COOKIE['data'])` PHP object injection vulnerable; JSON preferred, HMAC sign if serialize required
Business Logic Review
- Price manipulation — sending modified prices or quantities
- Workflow bypass — accessing step 3 without completing steps 1 and 2
- Race conditions — submitting two concurrent requests to exploit TOCTOU
- Privilege escalation through valid but unintended paths
- Data relationship violations — accessing another user's data through a shared resource
- Understand the intended flow thoroughly before reading code
- Ask: what is the application trying to prevent? then look for ways the code fails to prevent it
- Trace every possible path to sensitive operations, not just the happy path
- Look for missing state validation — operations that should require prior state but do not check it
Resources
- OWASP Code Review Guide (free)
- PortSwigger Web Security Academy (free)
- language-specific security documentation
- Secure Code Warrior (paid, language-specific training)
- "Iron-Clad Java" (book for Java secure coding)
Stage 04
Authentication and Cryptography Review
Authentication and cryptographic failures are among the most severe vulnerability classes and require specialized review knowledge.
Authentication Code Review
- Password storage — MD5, SHA-1, SHA-256 without salt insufficient; bcrypt (cost factor ≥ 10), Argon2id (time ≥ 1, memory ≥ 64MB), scrypt, PBKDF2 (iterations ≥ 600,000 for SHA-256) acceptable
- Password comparison — does it use constant-time comparison (`hmac.compare_digest`, `crypto.timingSafeEqual`) to prevent timing attacks?
- Session token generation — `os.urandom(32)` or `secrets.token_hex(32)`; not `random.random()` or timestamp-based
- Session ID in cookie — HttpOnly flag, Secure flag, SameSite=Strict or Lax
- Session regeneration — is a new session ID issued after login?
- Session invalidation — are sessions properly invalidated on logout?
- MFA TOTP secret storage — is it encrypted at rest?
- MFA recovery codes — single-use? stored hashed?
- MFA backup bypass — does "forgot password" flow bypass MFA?
- OAuth state parameter — is it validated on callback to prevent CSRF?
- OAuth redirect_uri validation — exact match required, not prefix/wildcard match
- OIDC nonce validation — prevents replay attacks
Cryptography Review
- Symmetric encryption — AES-256-GCM preferred; AES-CBC with PKCS#7 requires careful padding validation
- Asymmetric — RSA-2048+ or ECDSA with P-256/P-384; RSA-1024 is insufficient
- Hashing (integrity) — SHA-256 or SHA-3; SHA-1 for non-security uses only
- HMAC — HMAC-SHA256; note `SHA256(key + message)` is NOT a MAC (length extension attack)
- Key derivation — PBKDF2, bcrypt, Argon2; not SHA256(password)
- ECB mode — patterns visible in ciphertext; never use for block ciphers
- Reused IV/nonce — catastrophic for GCM, CTR; IV must be unique per encryption
- Hardcoded keys — encryption key in source code
- Client-side encryption — encrypted in browser, key sent to server = not encrypted
- Homegrown crypto — custom encryption algorithm instead of standard library
- `ssl.CERT_NONE` / `verify=False` — disabling certificate validation in HTTP clients
- Key storage — not in source code, config files, or committed env vars
- Key rotation — rotation procedure; re-encrypt old values after rotation
JWT Review
- JWT alg: none — signature bypassed entirely; library must reject this
- JWT alg: HS256 when RS256 expected — using public key as HMAC secret
- Verifier must explicitly specify expected algorithm, not accept whatever the token specifies
- JWT exp — expiration; is it checked? what is the maximum lifetime?
- JWT iss — issuer; is it validated against expected value?
- JWT aud — audience; is it validated to prevent token reuse across services?
- JWT sub — subject; does the application verify the subject matches the expected user?
- JWT payload is base64-encoded, not encrypted; never include passwords, secrets, credit card numbers
- JWK confusion — `jwks_uri` / `x5u` / `jku` header fetched from token = attacker-controlled key material
Resources
- OWASP Authentication Cheat Sheet (free)
- jwt.io (free, JWT debugging)
- NIST SP 800-63B (free)
- PortSwigger JWT labs (free)
Stage 05
Access Control and Authorization Review
Broken access control is the #1 OWASP category. Most authorization bugs require understanding business logic, not just pattern matching.
Authorization Review Methodology
- Map all resources and operations; for each resource × operation × role combination define what should be permitted
- Map all roles/user types: admin, regular user, guest, service account
- Read the code: does it enforce what the matrix requires?
Common Authorization Patterns and Flaws
- IDOR — `User.find(params[:id])` no ownership check; `current_user.owned_users.find(params[:id])` scoped to current user; any lookup using user-supplied ID without authorization check
- Horizontal privilege escalation — User A accesses user B's data by changing an ID; review every place a resource ID from user input is used
- Vertical privilege escalation — regular user accessing admin functions; common mistake: role stored in JWT and checked only at JWT decode, not re-validated against database
- Missing function-level authorization — API endpoint works but was intended only for admin; front-end-only access control is insufficient
- Path traversal in authorization — `/user/123/document` → `/user/123/../456/document` accessing another user's document via traversal
- Privilege in URL parameter — `?admin=true` or `?role=admin` privilege parameter that application reads
Object-Level Authorization (Row-Level)
- ORM-level — `Order.find(params[:id])` vulnerable; `current_user.orders.find(params[:id])` safe (only current user's orders)
- Raw SQL — `WHERE id = ?` vulnerable; `WHERE id = ? AND user_id = ?` safe
- GraphQL — field-level authorization, not just query-level
Function-Level Authorization (Endpoint-Level)
- Middleware applied globally easy to forget to exclude; per-route annotation easy to forget to add; best practice deny by default, explicitly permit
- REST API — every endpoint checks authentication + authorization
- GraphQL — every resolver checks authorization; not just at the query root
Resources
- OWASP Access Control Cheat Sheet (free)
- OWASP Authorization Testing Guide (free)
- PortSwigger Access Control labs (free)
Stage 06
Dependency and Supply Chain Review
Third-party dependencies are a major source of application vulnerabilities. Reviewers must evaluate both direct and transitive dependencies.
Dependency Review
- Are dependencies pinned to specific versions? Are lockfiles committed (package-lock.json, Pipfile.lock)?
- Are any new dependencies added? What is their provenance?
- Are deprecated or unmaintained packages being used?
- Are there known CVEs in direct dependencies?
- Dependency confusion attacks — internal packages prefixed to avoid namespace conflicts; review npm/pip/Maven registry config
- Package integrity — npm package-lock.json integrity hashes; pip `--hash=sha256:...`; Maven central repository integrity
- Review any custom-forked dependencies — could introduce backdoors; dependency pinning prevents silent updates
Supply Chain Code Patterns
- Eval on package data — calling eval on a field sourced from an imported package; dangerous if package is compromised
- Dynamic requires — `require(userInput)` dangerous if user-controlled
- Post-install scripts — `package.json scripts.postinstall` executes arbitrary code at install time
IaC Security Review
- S3 buckets — `acl = "private"`, `block_public_acls = true`
- Security groups — no `0.0.0.0/0` on port 22 (SSH) or port 3389 (RDP)
- IAM — no `"*"` in policy actions; least privilege
- Encryption — EBS volumes encrypted; RDS encrypted; S3 server-side encryption
- Logging — CloudTrail enabled; VPC flow logs enabled
- Secrets — no hardcoded credentials in Terraform files
Resources
- OWASP Dependency Check (free)
- Snyk documentation (free)
- CISA Supply Chain Risk Management guidance (free)
Stage 07
Writing Security Findings
A finding no one acts on is a finding that failed. Clear, actionable, developer-friendly security findings are the product of code review.
Finding Structure
- Title — specific, not generic: "SQL Injection in User Search Endpoint" not "SQL Injection"
- Severity — Critical/High/Medium/Low with CVSS score if required
- Location — exact file, line number, function name
- Description — what the vulnerability is; why it is exploitable
- Evidence — the specific vulnerable code snippet
- Impact — what an attacker can do if they exploit this
- Proof of Concept — for high/critical findings, a demonstration payload or reproduction steps
- Remediation — specific fix guidance with correct code example where possible
- References — OWASP, CWE, CVE, documentation links
Calibrating Severity
- Consider context, not just CVSS base score: authenticated endpoint, data sensitivity, internet-reachable, blast radius
- Avoid severity inflation — over-reporting medium issues as critical destroys credibility
- Avoid severity deflation — under-reporting critical issues to avoid conflict is worse
Developer Communication
- Lead with the fix, not the blame — "this pattern needs to change to X" not "this code is wrong"
- Code examples — show the correct implementation, not just the incorrect one
- Context — explain why the current code is insufficient; developers fix things they understand
- Prioritization guidance — what needs fixing immediately vs in the next sprint
- Availability — offer to answer questions; the goal is remediation, not a report
Resources
- OWASP Vulnerability Disclosure Cheat Sheet (free)
- CWE/CVE nomenclature (free)
- sample security reports from HackerOne Hacktivity (free)
Stage 08
Hands-On Practice & Portfolio
Practice
- GitHub open source code review — clone popular web applications, apply taint analysis manually, trace user input to sinks, report real findings as responsible disclosure
- PortSwigger Academy — complete all labs; each maps to a real code pattern
- HackTheBox web challenges — often involve source code review
- CyberDefenders code review challenges
- OWASP WebGoat — source code is available; review it before exploiting
- Find intentionally vulnerable code repositories (vAPI, DVPWA, VAmPI); review before exploiting
- Bug bounty source code review — many programs include mobile apps, open-source components, public GitHub repos; review JS bundles for hardcoded secrets, API endpoints, business logic
What to Document on LabList
- Annotated code review exercises — showing vulnerable code, the trace, the finding, and the remediation
- Language-specific checklists — custom checklists developed from review experience
- Open source responsible disclosure — finding and reporting a real vulnerability
- Review methodology documentation — your personal approach with language and pattern specifics
- Cert progression — Security+ → CSSLP or GWEB documented with connection to review work
FAQ
Common questions
How long does it take to become a Secure Code Reviewer?
2–3 years optimistic at 20–25 hours/week, 3–4 years realistic. Code review demands programming fluency in multiple languages (Java + Python + JavaScript is the common combination), then security pattern recognition layered on top. The fastest path is developer-to-AppSec-with-review-specialization. Pure security backgrounds without programming depth don't make it through screens because reviewers must read code faster than developers write it.
Which certifications matter for code review roles?
CSSLP for the secure development lifecycle frame. GWAPT or eWPT for offensive web testing depth. OSWE for advanced web exploitation context. The OWASP Code Review Guide (free) is the canonical methodology resource — treat it as a primary curriculum document. Cert market matters less than demonstrated review work.
Do I need a CS degree?
No, but you must read code fluently in at least two languages. Reviewers are tested with real code during interviews. Self-taught developers with strong reading practice compete effectively. What you do need: programming fluency (Java + Python + JavaScript at minimum), OWASP Top 10 at code level, business logic analysis instincts, and at least one SAST tool (Semgrep or CodeQL) at depth.
What separates a hired Code Reviewer?
Annotated code review writeups. Show the vulnerable code, the trace from source to sink, the finding writeup, and the remediation — for real open-source projects. Other differentiators: custom Semgrep rules on GitHub, language-specific vulnerability checklists, open source responsible disclosure history. Code review is listed as required skill in the majority of AppSec postings rather than a standalone role.