Skip to main content

Taint analysis

Vulkro runs three taint passes on every vulkro scan:

  1. Intra-procedural - within a single function.
  2. Inter-procedural, single-module - across functions in one file.
  3. Cross-file, call-graph-driven - follows the project's call graph, depth limited to 4.

A finding is emitted when tainted data - anything that flows from a source the engine considers user-controlled - reaches a sink the engine considers dangerous, without passing through a known sanitiser.

Sources, sinks, sanitisers

Sources include:

  • req.body, req.query, req.params, req.headers (Express / Next.js)
  • request.json(), request.form(), request.query_params (FastAPI)
  • flask.request.*, Django request.GET / POST
  • Function parameters of route handlers
  • Reads from os.environ / process.env (configurable; off by default to keep the noise sane)

Sinks include:

  • db.query("..." + tainted), cursor.execute(...) (SQL injection)
  • child_process.exec, subprocess.run(shell=True) (command injection)
  • eval, Function(...), pickle.loads, yaml.load(..., Loader=yaml.Loader)
  • requests.get(tainted), fetch(tainted) (SSRF)
  • redirect(tainted) (open redirect)
  • Template-as-string with user data (SSTI / XSS)
  • __proto__ writes (prototype pollution)

Sanitisers include:

  • Parameterised queries (? / $1 / :name)
  • html.escape / bleach.clean
  • Allowlist-shaped enum / Pydantic / Zod schemas
  • urllib.parse.quote for URLs
  • Constant-time comparison wrappers

Cross-file propagation

A typical case the cross-file pass catches:

# routes/admin.py
@router.post("/admin/export")
def export(req):
data = req.json() # SOURCE
return run_export(data["filter"])

# services/exports.py
def run_export(filter_expr: str):
return db.execute(f"SELECT * FROM logs WHERE {filter_expr}") # SINK

The intra-procedural pass alone would miss this - req.json() and the SQL sink are in different files. The cross-file pass traces the call from export to run_export and propagates the tainted argument.

Depth limit is 4 calls deep; deeper paths are skipped to keep wall-clock bounded on large repos.

Escape hatch

If a regression in cross-file taint blocks your build, set:

VULKRO_DISABLE_INTERPROC_TAINT=1 vulkro scan .

Skips just the cross-file pass; the intra-procedural and single-module passes still run.