Tainted Trails: Unmasking Vulnerabilities with Source and Sink Analysis

Tainted Trails: Unmasking Vulnerabilities with Source and Sink Analysis

There’s a single idea underneath almost every code-level vulnerability finding you’ll ever make: something untrusted got somewhere dangerous without being properly checked along the way. That’s it. That’s source and sink analysis, and every SQL injection, every command injection, every path traversal, every SSRF bug is a specific instance of that one pattern. I want to walk through the discipline of source-and-sink (taint) analysis as a unified whole — how to think about it, how to do it well by hand, and how to scale it with tooling, because this is genuinely the single most transferable skill in vulnerability research.

The Core Concept: Taint Propagation

“Taint” is a label applied to data that originates from an untrusted source. As that data moves through a program — gets concatenated with other strings, passed as a function argument, stored in an object field, written to and read back from a database — the taint label should, in principle, propagate along with it, until either:

  • It reaches a sink, a dangerous operation, while still tainted → vulnerability
  • It passes through a genuine sanitizer that removes or neutralizes the dangerous characteristics of the data → taint cleared

This sounds mechanical, but the subtlety is almost entirely in correctly modeling propagation and sanitization — under-modeling propagation causes false negatives (missed vulnerabilities), and over-modeling sanitization causes false negatives too (assuming something is safe when it isn’t).

flowchart TD
    A[Source: Untrusted Input] --> B{Sanitized?}
    B -->|No| C[Taint Propagates Through Program]
    C --> D{Reaches a Sink?}
    D -->|Yes| E[Vulnerability Confirmed]
    D -->|No| F[Dead End - No Impact]
    B -->|Yes, Properly| G[Taint Cleared]
    G --> H[Safe to Reach Sink]

Cataloging Sources

Just as sinks need a catalog (covered in depth elsewhere), sources need one too, and it’s easy to under-scope this list to “just HTTP parameters.” A thorough source catalog includes:

Source CategoryExamples
Direct user inputHTTP query params, form fields, JSON bodies, headers, cookies
File-based inputUploaded files, config files with external write access, imported data files
Network inputSocket data, message queue payloads, webhook bodies
EnvironmentEnvironment variables, command-line arguments
Indirect/second-orderDatabase fields previously populated by untrusted input, cached values, log entries later parsed
Third-party integrationAPI responses from partner systems, OAuth token claims, DNS responses

That last category — indirect or second-order sources — is where a lot of experienced auditors separate themselves from beginners. Data doesn’t have to come directly from the current request to be tainted; if it was untrusted when it entered the system at all, and nothing has genuinely validated it since, it’s still tainted when it resurfaces.

Manual Taint Tracing: A Worked Example

Consider this Node.js snippet, tracing a path traversal vulnerability from source to sink:

// SOURCE
app.get('/download', (req, res) => {
  const filename = req.query.file;               // tainted: user-controlled
  serveFile(filename, res);
});

function serveFile(filename, res) {
  const safeName = filename.replace(/\.\./g, ''); // attempted sanitization
  const fullPath = path.join(UPLOAD_DIR, safeName);
  res.sendFile(fullPath);                         // SINK: filesystem access
}

At first glance, the replace(/\.\./g, '') call looks like a sanitizer stripping directory traversal sequences. But this is a textbook example of insufficient sanitization: an input like ....//....//etc/passwd has its .. sequences removed once, but the remaining characters recombine into a new .. sequence — ....// becomes ../ after a single non-overlapping replace pass. A correct taint analysis has to recognize that this sanitizer doesn’t actually clear the taint; the vulnerability survives the “sanitization” step. This exact class of incomplete regex-based path sanitization has appeared in real CVEs repeatedly.

Source-Sink Pairing Table by Vulnerability Class

Vulnerability ClassTypical SourceTypical SinkEffective Sanitization
SQL InjectionHTTP params, form dataRaw SQL query constructionParameterized queries/prepared statements (not string escaping alone)
Command InjectionHTTP params, filenames, env varsexec/system/subprocess callsAvoid shell invocation entirely; use argument arrays, not string concatenation
Path TraversalFilenames, URL pathsFile open/read/write operationsCanonicalize path, then verify it’s within an allowed base directory
SSRFURLs, hostnames from user inputOutbound HTTP requests, DNS resolutionAllow-list destinations; resolve and validate IP before connecting
XXEXML document bodiesXML parser with external entity resolutionDisable DTD/external entity processing at the parser level
Insecure DeserializationSerialized blobs, cookies, cache valuespickle.loads, readObject, unserializeAvoid native deserialization of untrusted data; use schema-validated formats
SSTIUser-controlled strings used as template contentTemplate engine render() callsNever pass user input as the template itself; only as template variables

From Manual to Automated: Taint Analysis Tooling

Doing this by hand, function by function, works but doesn’t scale past a small codebase. Automated static taint analysis tools essentially formalize the process above:

  • Interprocedural taint tracking: following tainted values across function and even file boundaries, building a full call graph annotated with taint status at each point.
  • Sanitizer modeling: tools need an explicit list of what functions count as sanitizers for which sink types — this list needs constant maintenance because custom, project-specific sanitizer functions are common and won’t be recognized out of the box.
  • Path-sensitivity: better tools account for conditional branches — a value might be tainted on one code path and safely validated on another, and treating them identically produces false positives.
# Example: running Semgrep with a taint-mode rule against a codebase
semgrep --config p/security-audit --config custom-taint-rules.yaml ./src

A representative custom Semgrep taint rule (conceptual YAML) for the path traversal example above:

rules:
  - id: insufficient-path-traversal-sanitization
    mode: taint
    pattern-sources:
      - pattern: req.query.$FIELD
    pattern-sanitizers:
      - pattern: path.resolve($X).startsWith(SAFE_BASE)
    pattern-sinks:
      - pattern: res.sendFile($PATH)
    message: "Tainted filename reaches sendFile() without proper base-directory validation"
    languages: [javascript]
    severity: ERROR

Notice the sanitizer pattern here is deliberately strict — it requires an explicit startsWith check against a known-safe base directory after path resolution, not just a regex-based character strip, which correctly rejects the flawed .replace(/\.\./g, '') pattern from the earlier example as insufficient.

Real-World Case Study: Second-Order SQL Injection

A pattern worth walking through because it’s easy to miss: an application properly parameterizes SQL queries at the point where a user directly submits a “display name” during registration — no first-order SQL injection there. But a separate administrative reporting feature later reads that stored display name and, for performance reasons, builds a raw SQL string to generate a summary report, concatenating the display name directly into the query. The original taint — the fact that the display name originated from untrusted user input — never gets “cleared,” it just goes dormant in the database until a second, unrelated code path reintroduces the risk. This is exactly why source and sink analysis needs to consider the database (and caches, message queues, log files) as a pass-through, not a trust boundary that automatically clears taint.

sequenceDiagram
    participant U as User
    participant App as Registration API
    participant DB as Database
    participant Rep as Reporting Module

    U->>App: Submit crafted input
    App->>DB: Safe parameterized insert
    Note over DB: Payload stored as data

    Rep->>DB: Read stored value
    Rep->>DB: Unsafe query construction
    Note over Rep: Second-order SQL injection

Defensive Best Practices

  • Treat all data originating outside your direct control as tainted for the lifetime of the application, not just for the duration of the request that introduced it — including anything read back out of a database, cache, or log.
  • Use context-appropriate, structural sanitization, not blocklist-based string manipulation. Parameterized queries, not string escaping, for SQL. Argument arrays, not shell string construction, for command execution. Canonical path resolution plus base-directory verification, not regex stripping, for path traversal.
  • Model sanitizers precisely in your tooling. A sanitizer that’s valid for one sink type (HTML-encoding for XSS) is frequently invalid for another (the same value used in a shell command) — taint tools need to track which kind of taint has been cleared, not just whether “some” sanitization happened.
  • Pay special attention to second-order/stored injection paths — audit not just “where does input directly flow” but “everywhere this stored value is later read and used.”
  • Combine static taint analysis with dynamic confirmation. Static tools are excellent at surfacing candidates; a working proof-of-concept is what actually confirms exploitability and rules out false positives from imperfect sanitizer modeling.

Frequently Asked Questions

What’s the difference between taint analysis and generic static analysis? Generic static analysis covers a broad range of code quality and correctness checks (unused variables, type errors, style issues). Taint analysis is a specific technique within static analysis focused exclusively on tracking untrusted data flow from sources to sinks — it’s a subset, purpose-built for security-relevant data flow bugs.

Can taint analysis have false negatives even with good tooling? Yes, commonly through dynamic language features (reflection, eval-style constructs, dynamic property access), inter-process/inter-service data flow that a single-codebase tool can’t see, and custom sanitizer functions the tool hasn’t been configured to recognize.

Is manual taint tracing still a useful skill given how mature automated tools have become? Very much so — automated tools need correctly configured sources, sinks, and sanitizers to be effective, and building that configuration for an unfamiliar or highly custom codebase requires the same manual tracing skill. Manual tracing is also essential for confirming and explaining findings that automated tools surface, since a raw tool alert without a demonstrated exploit path is often not actionable on its own.

How does taint analysis relate to fuzzing? They’re complementary: taint analysis (static) tells you where untrusted data can theoretically reach a dangerous sink; fuzzing (dynamic) actually exercises that code path with crafted input to confirm the vulnerability triggers real, observable impact — like a crash, an unauthorized query result, or command execution.

References

Summary and Recommendations

Source and sink analysis is the connective thread running through almost every meaningful vulnerability class — the specific vulnerability type just describes which sink got reached and how. Mastering it means building a genuinely complete catalog of sources and sinks for your stack, understanding that taint doesn’t clear just because data passed through a database or a cache, and being rigorous about whether a “sanitizer” is structurally correct for the specific sink it’s protecting, rather than assuming any input filtering is good enough. Do this well by hand first, then scale it with static taint analysis tooling — and always confirm what the tooling surfaces with a real, working proof-of-concept before calling it a finding.

Total
10
Shares

Leave a Reply

Previous Post
How to Select the Right Target for Vulnerability Research: A Practical Guide

How to Select the Right Target for Vulnerability Research: A Practical Guide

Next Post
Navigating the Maze: Mastering Sink-to-Source Vulnerability Analysis

Navigating the Maze: Mastering Sink-to-Source Vulnerability Analysis

Related Posts