Prerequisites
- Target system, dependencies and environment configured.
Usage
Purpose
Most serious code-level bugs are the same shape: untrusted data reaches a dangerous operation without being neutralised on the way. Taint tracking is the method for finding them by reading — start at a source (where attacker-controlled data enters), follow it to a sink (where it can cause harm), and check whether anything sanitises it in between. This skill is the core technique the rest of the secure-code-review domain builds on.
When to use it
Reviewing a PR or auditing a codebase for injection-class flaws (SQLi, command injection, XSS, path traversal, SSRF, deserialization). It's how you turn "this looks risky" into "this input reaches this sink unsanitised, here's the path".
The model
- Source — where untrusted data enters: request parameters, headers, cookies, uploaded files, message queues, external API responses, even the database if it holds user-influenced data.
- Sink — where tainted data becomes dangerous: SQL query, shell/exec call, HTML output, filesystem path, deserializer, redirect target, LDAP query.
- Sanitiser — what makes it safe in between: parameterisation, output encoding, allowlist validation, safe APIs. A path is exploitable when a source reaches a sink with no effective sanitiser.
Procedure
- Find the sinks first — there are fewer of them than sources, so it's faster to work backwards. Grep for the dangerous operations in the language you're reviewing:
rg -n 'execute\(|exec\(|system\(|eval\(|innerHTML|readFile|deserialize|Runtime.getRuntime'
- For each sink, ask: what data reaches it? Trace the argument backwards through variables and function calls to its origin.
- Determine whether that origin is a source — is any part of it attacker-controlled? If it's a constant or fully internal value, move on. If it traces back to a request, it's tainted.
- Check the path for sanitisation. Between source and sink, is the data parameterised, encoded for the sink's context, or validated against an allowlist? Note that the wrong sanitiser doesn't count — HTML-encoding data that flows into a SQL query does nothing.
- Confirm exploitability before reporting. A tainted path guarded by a genuine, correct sanitiser is safe; one with none, or with a bypassable blocklist, is a finding. Write it up as the concrete path: source → (no/weak sanitiser) → sink.
- Use a tool to widen coverage, then verify by hand — automated taint analysis finds candidates and false positives, human reading confirms which are real:
semgrep --config p/owasp-top-ten
Cheatsheet
SQL: rg -n 'execute|createQuery|rawQuery|\$where'
Command: rg -n 'exec|system|popen|ProcessBuilder|Runtime'
XSS: rg -n 'innerHTML|dangerouslySetInnerHTML|render_template_string|\|safe'
Path: rg -n 'open\(|readFile|File\(|sendFile|include'
Deser: rg -n 'pickle.loads|readObject|yaml.load|Marshal.load'
SSRF: rg -n 'requests.get|urlopen|fetch\(|HttpClient|curl'
rg -n 'request\.|params|getParameter|req\.(query|body|params)|os.environ|argv'
Reading the flow
- Source → sink, nothing in between = confirmed injection path. Report it with the exact lines.
- A sanitiser present but wrong-context (HTML escaping before a SQL sink,
int cast that's later concatenated as string elsewhere) = still vulnerable. The presence of a sanitiser isn't safety.
- A blocklist/regex "sanitiser" = suspect it until proven complete; blocklists usually miss an encoding or case. Weigh it as likely-bypassable.
- Tainted data stored then later read into a sink = second-order injection; the source is the earlier write, easy to miss if you only look one hop back.
- A path fully guarded by parameterisation or correct encoding = safe; note it so you don't re-flag it.
Making it stick (guidance for the fix)
The remediation is per-sink and lives in the specific skills (SQLi → parameterise, XSS → encode at output, etc.). The review-level habit: neutralise at the sink, for the sink's context, not with a generic input filter far away — because the same value can be safe in one sink and dangerous in another. Prefer safe-by-construction APIs (prepared statements, auto-escaping templates, safe deserializers) so the sanitiser can't be forgotten.
Pitfalls
- Starting from sources. There are too many; you'll drown. Start from the smaller set of sinks and work back.
- Stopping one hop back. Data often passes through several functions and stores; follow it to the true origin, including stored/second-order paths.
- Accepting any sanitiser as sufficient. Wrong-context or blocklist "sanitisers" give false comfort. Check it matches the sink.
- Trusting the SAST result wholesale. It's a candidate list with false positives and negatives — confirm each path by reading it.
References
- OWASP Code Review Guide
- OWASP Top 10 (Injection classes) and the per-class prevention cheat sheets
- Semgrep documentation and rule registry
- CWE-20 (Improper Input Validation), CWE-74 (Injection)
Inputs
- Relevant source code, logs, network traces, or system specifications.
Outputs
- Analysis findings, security audit report, or generated code artifacts.
1---2name: taint-tracking-by-hand3description: Use when reviewing source for injection-class bugs — following untrusted input from where it enters (source) to where it does damage (sink) to decide if a path is exploitable.4---5678## Prerequisites9- Target system, dependencies and environment configured.1011## Usage12### Purpose1314Most serious code-level bugs are the same shape: untrusted data reaches a dangerous operation without being neutralised on the way. Taint tracking is the method for finding them by reading — start at a **source** (where attacker-controlled data enters), follow it to a **sink** (where it can cause harm), and check whether anything sanitises it in between. This skill is the core technique the rest of the secure-code-review domain builds on.1516### When to use it1718Reviewing a PR or auditing a codebase for injection-class flaws (SQLi, command injection, XSS, path traversal, SSRF, deserialization). It's how you turn "this looks risky" into "this input reaches this sink unsanitised, here's the path".1920### The model2122- **Source** — where untrusted data enters: request parameters, headers, cookies, uploaded files, message queues, external API responses, even the database if it holds user-influenced data.23- **Sink** — where tainted data becomes dangerous: SQL query, shell/exec call, HTML output, filesystem path, deserializer, redirect target, LDAP query.24- **Sanitiser** — what makes it safe in between: parameterisation, output encoding, allowlist validation, safe APIs. A path is exploitable when a source reaches a sink with **no effective sanitiser**.2526### Procedure27281. **Find the sinks first** — there are fewer of them than sources, so it's faster to work backwards. Grep for the dangerous operations in the language you're reviewing:29 ```30 rg -n 'execute\(|exec\(|system\(|eval\(|innerHTML|readFile|deserialize|Runtime.getRuntime'31 ```322. For each sink, ask: **what data reaches it?** Trace the argument backwards through variables and function calls to its origin.333. Determine whether that origin is a **source** — is any part of it attacker-controlled? If it's a constant or fully internal value, move on. If it traces back to a request, it's tainted.344. **Check the path for sanitisation.** Between source and sink, is the data parameterised, encoded for the sink's context, or validated against an allowlist? Note that the *wrong* sanitiser doesn't count — HTML-encoding data that flows into a SQL query does nothing.355. **Confirm exploitability** before reporting. A tainted path guarded by a genuine, correct sanitiser is safe; one with none, or with a bypassable blocklist, is a finding. Write it up as the concrete path: source → (no/weak sanitiser) → sink.366. Use a tool to widen coverage, then verify by hand — automated taint analysis finds candidates and false positives, human reading confirms which are real:37 ```38 semgrep --config p/owasp-top-ten39 ```4041### Cheatsheet4243```bash44SQL: rg -n 'execute|createQuery|rawQuery|\$where'45Command: rg -n 'exec|system|popen|ProcessBuilder|Runtime'46XSS: rg -n 'innerHTML|dangerouslySetInnerHTML|render_template_string|\|safe'47Path: rg -n 'open\(|readFile|File\(|sendFile|include'48Deser: rg -n 'pickle.loads|readObject|yaml.load|Marshal.load'49SSRF: rg -n 'requests.get|urlopen|fetch\(|HttpClient|curl'5051rg -n 'request\.|params|getParameter|req\.(query|body|params)|os.environ|argv'5253```5455### Reading the flow5657- **Source → sink, nothing in between** = confirmed injection path. Report it with the exact lines.58- **A sanitiser present but wrong-context** (HTML escaping before a SQL sink, `int` cast that's later concatenated as string elsewhere) = still vulnerable. The presence of *a* sanitiser isn't safety.59- **A blocklist/regex "sanitiser"** = suspect it until proven complete; blocklists usually miss an encoding or case. Weigh it as likely-bypassable.60- **Tainted data stored then later read into a sink** = second-order injection; the source is the earlier write, easy to miss if you only look one hop back.61- **A path fully guarded by parameterisation or correct encoding** = safe; note it so you don't re-flag it.6263### Making it stick (guidance for the fix)6465The remediation is per-sink and lives in the specific skills (SQLi → parameterise, XSS → encode at output, etc.). The review-level habit: **neutralise at the sink, for the sink's context**, not with a generic input filter far away — because the same value can be safe in one sink and dangerous in another. Prefer safe-by-construction APIs (prepared statements, auto-escaping templates, safe deserializers) so the sanitiser can't be forgotten.6667### Pitfalls6869- **Starting from sources.** There are too many; you'll drown. Start from the smaller set of sinks and work back.70- **Stopping one hop back.** Data often passes through several functions and stores; follow it to the true origin, including stored/second-order paths.71- **Accepting any sanitiser as sufficient.** Wrong-context or blocklist "sanitisers" give false comfort. Check it matches the sink.72- **Trusting the SAST result wholesale.** It's a candidate list with false positives and negatives — confirm each path by reading it.7374### References7576- OWASP Code Review Guide77- OWASP Top 10 (Injection classes) and the per-class prevention cheat sheets78- Semgrep documentation and rule registry79- CWE-20 (Improper Input Validation), CWE-74 (Injection)8081## Inputs82- Relevant source code, logs, network traces, or system specifications.8384## Outputs85- Analysis findings, security audit report, or generated code artifacts.