September 20, 2026
How does a SAST scanner decide which line of code to flag?
A SAST scanner parses source code into a syntax tree, follows how values move through it, and flags the exact line where a tainted input…

By Codeunderfire
5 min read
A SAST scanner parses source code into a syntax tree, follows how values move through it, and flags the exact line where a tainted input such as a request parameter reaches a sink such as a SQL executor without a sanitizer on the path. The analysis is an approximation by theorem, which is why Google admits a check only under a 10 percent effective false-positive rate.
What a static scanner reads, and what it never touches
Static application security testing, SAST, is the class of tools that inspects source code, bytecode or a compiled binary for security flaws without executing the program. The OWASP community page on source code analysis tools defines the category by that constraint and by its output: a filename, a line number and a snippet, so a developer can go straight to the code.
Two neighbouring tool classes are defined by what they can touch instead. Dynamic application security testing sends requests at a deployed build and observes responses, so it sees one environment and nothing that never executes. Software composition analysis reads the dependency manifest and matches package versions against a vulnerability database, so it sees third-party code and none of yours.
Keep one handler in mind for the rest of this piece. A Flask endpoint reads account_id from the query string, builds a SELECT by string concatenation, and passes the result to cursor.execute. That handler has a defect catalogued as CWE-89, SQL injection, which sat at number three on the 2023 CWE Top 25. A scanner will mark the execute line. The rest of this article explains how it earns that mark and where the mark can be wrong.
How a request parameter becomes a flagged query
The scanner begins by parsing every file into an abstract syntax tree, then linking the trees into a control-flow graph for execution order and a data-flow graph for how values move between variables. Rules run over those graphs, and the rule family that catches injection is taint analysis.
Semgrep's taint mode documentation lays out the vocabulary the whole industry shares. A rule declares pattern-sources, places where untrusted data enters, such as a request argument. It declares pattern-sinks, calls where tainted data does damage, such as a query executor or a shell invocation. It declares pattern-sanitizers, functions that strip the taint, such as a parameter binder or an encoder. Taint propagates by default through assignments, operators and function calls, and pattern-propagators let a rule author add flows the engine would not infer, for example a strcpy that copies from one buffer into another.
Apply that to the handler. request.args["account_id"] matches a source. The concatenation on the next line propagates the taint into the sql string. cursor.execute(sql) matches a sink, and no sanitizer sat on the path between them. Semgrep reports the result as one taint trace per finding: a single source-to-sink path, even when several paths exist. That trace, pinned to the sink line, is the finding a reviewer sees.
One detail from the same documentation matters for triage later. Sources match every subexpression by default, while sinks match only the exact expression unless the rule says otherwise. A rule that names the wrong granularity produces either a flood of findings or silence.
Why the mark is an approximation and not a proof
A scanner that reads every branch sounds like it should deliver certainty, and the mathematics says otherwise. Rice's theorem, proved in 1953, states that no algorithm can decide a non-trivial semantic property of an arbitrary program. Whether a given value can ever reach cursor.execute is exactly such a property, so every analyzer must approximate in one of two directions.
Over-approximation assumes a value flows anywhere it might and reports paths no real input can trigger. Those are false positives. Under-approximation drops paths the engine cannot resolve, such as a call through an interface with a dozen implementations or a value produced by reflection, and stays silent about real bugs. Those are false negatives.
Scope is the practical dial. Semgrep's propagators work only inside a single function by default; cross-function tracking within a file needs the --pro-intrafile flag, and cross-file tracking needs interfile: true on a supported language. CodeQL draws the same line between local data flow inside one function and global data flow across calls, and warns that global analysis costs far more time. Each vendor picks a point on that curve.
The OWASP Benchmark measures the pick. Version 1.2 contains 2,740 test cases across 11 vulnerability categories, and a tool's score is its true positive rate minus its false positive rate. A tool that flags everything scores zero, which is the point of subtracting.
How much noise a team will tolerate
The research on why developers abandon static analysis has been consistent for over a decade. In 2013, researchers at North Carolina State University interviewed developers and found the leading complaints were false positives and warnings that did not explain themselves. In 2016, Microsoft surveyed its own engineers and found most would tolerate a false-positive rate no higher than about 15 to 20 percent before they stopped trusting a tool.
Google's account of its own program is the most quoted. An early deployment of FindBugs posted results to a dashboard outside the engineering workflow, and the dashboard went unread. The replacement, Tricorder, surfaces each finding as a comment in code review on the exact line, with a button that lets the reviewer mark it not useful. A check is allowed into the system only if its effective false-positive rate stays under 10 percent.
The word effective is doing real work. An effective false positive is any finding the developer declines to act on, whether or not the tool was technically right. The scanner can be correct that account_id reaches execute and still be wrong that anyone should care, because the endpoint is internal, the value is a constant in every caller, or the handler is dead code. Every such finding spends trust, and the trust runs out before the real finding is read.
What a clean report leaves out
A clean SAST report has a narrow meaning: no known dangerous pattern in your own code reached a known sink on a path the tool could follow. Four things sit outside that sentence.
Dependencies are the largest. CVE-2021–44228, Log4Shell, was a JNDI lookup inside Apache Log4j 2 from version 2.0-beta9 through 2.14.1, scored 10.0 on CVSS. An application that logged a user-supplied string was exploitable, and a scan of that application's own source showed an ordinary logging call, because the dangerous code lived in a jar the scanner was never asked to read. Finding it is software composition analysis work.
OWASP's page lists the rest. SAST tools are "frequently unable to find configuration issues, since they are not represented in the code," so a debug flag left on in production or a permissive CORS header never appears. The page also names authentication problems, access control issues and insecure use of cryptography as classes that are difficult to automate. A scanner can see that a handler loads a record by id; it has no way to know that the current user should not be allowed to load that record. Authorization is a property of the business, not of the syntax.
Triage the finding, then change two lines
When the finding lands on a pull request, read it in the order the engine built it. Confirm the source is untrusted: a query-string argument qualifies, a value from an operator-owned config file usually does not. Confirm the sink is the dangerous call the rule claims: cursor.execute with a built string qualifies, a log statement does not. Then walk the trace looking for a sanitizer the rule did not recognise. Teams that write their own escaping helper generate a whole family of findings until that helper is registered under pattern-sanitizers, which the Semgrep documentation covers alongside sources and sinks.
If all three checks hold, the finding is real, and the repair for the handler is a parameterized query. The concatenation line goes; the execute call takes a placeholder and passes account_id as a bound parameter, so the database treats the value as data and never as syntax. On the next scan the trace from the request argument passes through the binding, and the mark disappears.
What to do next depends on where the scanner runs. Put it on every pull request so the finding sits on the diff the author is already reading. Baseline the existing codebase on day one so nobody receives ten thousand findings from untouched files. Block the merge only on rules whose precision you have measured, and track the fix rate on findings rather than the count, because a scanner that reports more and gets fixed less has made the code less safe.
This article expands on our video "What is SAST and how does static application security testing work?" — watch it here.
Source: Semgrep: Taint analysis (taint mode).