Taint Analysis: How SAST Follows Untrusted Data to a Sink
Strip away the marketing and most of a static analyser's genuinely useful output comes from one technique: taint analysis. It is a deceptively simple model — some data is untrusted, some operations are dangerous, and a bug is any path that connects the two without cleansing in between. Understanding it explains both why SAST catches injection so well and why it produces the false positives that drive teams up the wall.
Taint analysis rests on three concepts. A source is any point where untrusted data enters the program. A sink is any operation where that data can cause harm. A sanitizer is code that neutralises the danger. The analyser marks values from sources as “tainted,” propagates that mark through assignments and function calls, and raises a finding when a tainted value reaches a sink without passing through an appropriate sanitizer.
Sources, sinks, and the space between
- Sources are the program's edges with the outside world: HTTP parameters, headers and cookies, request bodies, uploaded files, database reads of user-supplied data, environment variables, and message-queue payloads.
- Sinks are the dangerous operations: SQL query execution,
Runtime.execand shell calls, filesystem paths, HTML rendering, deserialisation, and calls that build further code or queries. - Sanitizers break the chain: parameterised queries, output encoders, allow-list validators, and canonicalisation routines that make the value safe for its destination.
String q = "SELECT * FROM users WHERE name = '" + name + "'";
stmt.executeQuery(q); // sink: tainted value reaches SQL unsanitised
Propagation is where it gets hard
Following taint across a single method is easy. Following it across an entire program — through helper functions, class fields, collections, and library calls — is interprocedural analysis, and it is where engines earn their keep. When a tainted string is put into a list and pulled out three functions later, the analyser must carry the mark the whole way. Every framework abstraction, callback, and reflective call is a chance to lose the thread.
Over- and under-approximation
No analyser is both complete and sound in practice. Lean toward over-approximation and you catch more real bugs but flag safe code as vulnerable — false positives. Lean toward under-approximation and you stay quiet on safe code but miss real flow — false negatives. The most damaging misses come from unrecognised sanitizers: your team wraps every query in a validated helper, the tool doesn't know that helper cleans the value, and it either screams about safe code or, if configured to trust the wrapper, goes silent on a genuinely broken call path.
This is why serious SAST configuration is largely about teaching the engine your sources, sinks, and sanitizers. A custom validation function that the tool treats as a sanitizer collapses a wall of false positives; a home-grown template renderer declared as a sink surfaces XSS the default rules would miss entirely.
Reading a taint finding well
When you triage a taint result, the trace matters more than the verdict. Follow the reported path from source to sink and ask two questions: is the source genuinely attacker-controlled, and does the value truly reach the sink uncleansed? Half of dismissed findings are a sanitizer the tool didn't recognise — which is a signal to teach it, not just to suppress. Half of confirmed ones are a wrapper everyone assumed was safe.
Taint analysis is only as good as your inventory of where untrusted data enters and where it can do damage. Map those edges deliberately — because the flows you never declared are the ones the scanner will silently let through.
- Take one noisy SAST finding and follow its source-to-sink trace: is the source genuinely attacker-controlled, and does the value reach the sink uncleansed? Half of dismissals are a sanitizer the tool did not recognise.
- Check whether your custom validation helpers are declared to the engine as sanitizers, or whether it is flagging every safe call downstream.
Triaging one trace is instructive; tuning the engine to your real sources, sinks, and sanitizers across the codebase is what our assessment runs for you.