Security code review
A security review follows tainted data, not code style. One question repeats at
every line: can attacker-controlled input reach a dangerous operation without
being neutralized on the way? Miss the path and the bug ships looking completely
ordinary.
Method
- List sources and sinks before reading logic. Sources are where untrusted
data enters: request params, headers, cookies, uploaded files, queue
messages, third-party responses, the environment. Sinks are where it does
harm: SQL, shell, filesystem paths, HTML output, redirects, deserializers,
template engines, reflection.
- Trace each source to each sink. Follow the variable through assignments
and calls. Taint clears only at a real sanitizer for that exact sink: a
parameterized query, a context-aware encoder, path canonicalization plus an
allowlist. A type cast is not sanitization.
- Run the boundary checklist at every trust edge: input validated against
an allowlist, output encoded for its context, authentication present,
authorization confirming ownership, size and rate bounded, errors that do not
leak internals. A gap in any one of those is a finding.
- Split injection checks by interpreter. SQL: bound parameters, never
concatenation. Shell: argument arrays, never a string command, never
shell=True on user input. HTML: encode for context (body versus attribute
versus URL versus script). No interpreter ever eats a string built by
formatting user data.
- Check authorization at the object level. For every id read from the
request, confirm the code proves the caller may touch that specific object.
Missing this is IDOR, the most common real finding and invisible to a linter.
- Name the quiet sinks: open redirects, SSRF through user-supplied URLs,
XXE in XML parsers, prototype pollution, unsafe deserialization (pickle,
native Java,
yaml.load). None look dangerous until you know the shape.
- Write each finding as a path, source to sink. "
req.query.file reaches
fs.readFile at line 40 with no canonicalization, so ../../ reads any
file." A finding with no traceable path is a guess in a bug's clothing.
Checks
- For each user input in the diff, can you name where it lands and exactly what
sanitizes it before it gets there?
- Does every request-supplied id get an ownership check before it is used?
- Are all SQL and shell calls parameterized, with no string concatenation of
input anywhere?
Boundaries
This is the how-to-read companion to security-review, which ranks findings by
exploitability. Taint engines (CodeQL, Semgrep) surface candidate paths but miss
authorization logic entirely, so a human reviewer owns that judgment.
1---2name: security-code-review3description: Read a diff for security by tracing attacker-controlled input to dangerous operations and checking every trust boundary it crosses. Use when reviewing code that handles input, queries, files, output rendering, or authorization.4---56# Security code review78A security review follows tainted data, not code style. One question repeats at9every line: can attacker-controlled input reach a dangerous operation without10being neutralized on the way? Miss the path and the bug ships looking completely11ordinary.1213## Method14151. **List sources and sinks before reading logic.** Sources are where untrusted16 data enters: request params, headers, cookies, uploaded files, queue17 messages, third-party responses, the environment. Sinks are where it does18 harm: SQL, shell, filesystem paths, HTML output, redirects, deserializers,19 template engines, reflection.202. **Trace each source to each sink.** Follow the variable through assignments21 and calls. Taint clears only at a real sanitizer for that exact sink: a22 parameterized query, a context-aware encoder, path canonicalization plus an23 allowlist. A type cast is not sanitization.243. **Run the boundary checklist at every trust edge:** input validated against25 an allowlist, output encoded for its context, authentication present,26 authorization confirming ownership, size and rate bounded, errors that do not27 leak internals. A gap in any one of those is a finding.284. **Split injection checks by interpreter.** SQL: bound parameters, never29 concatenation. Shell: argument arrays, never a string command, never30 `shell=True` on user input. HTML: encode for context (body versus attribute31 versus URL versus script). No interpreter ever eats a string built by32 formatting user data.335. **Check authorization at the object level.** For every id read from the34 request, confirm the code proves the caller may touch that specific object.35 Missing this is IDOR, the most common real finding and invisible to a linter.366. **Name the quiet sinks:** open redirects, SSRF through user-supplied URLs,37 XXE in XML parsers, prototype pollution, unsafe deserialization (pickle,38 native Java, `yaml.load`). None look dangerous until you know the shape.397. **Write each finding as a path, source to sink.** "`req.query.file` reaches40 `fs.readFile` at line 40 with no canonicalization, so `../../` reads any41 file." A finding with no traceable path is a guess in a bug's clothing.4243## Checks4445- For each user input in the diff, can you name where it lands and exactly what46 sanitizes it before it gets there?47- Does every request-supplied id get an ownership check before it is used?48- Are all SQL and shell calls parameterized, with no string concatenation of49 input anywhere?5051## Boundaries5253This is the how-to-read companion to security-review, which ranks findings by54exploitability. Taint engines (CodeQL, Semgrep) surface candidate paths but miss55authorization logic entirely, so a human reviewer owns that judgment.