How Taint Tracking Analyzes Data Flow from Sources to Sinks in NVIDIA SkillSpector

SkillSpector’s behavioral taint‑tracking analyzer parses Python ASTs to mark variables originating from source calls as tainted, propagates that taint through assignments and data structures, and reports findings when tainted data reaches sink calls.

NVIDIA SkillSpector implements a sophisticated behavioral taint‑tracking system that analyzes how untrusted data flows from entry points (sources) to vulnerable operations (sinks). Understanding how taint tracking analyzes data flow from sources to sinks helps security teams automatically identify credential leaks, data exfiltration pathways, and remote code execution risks in Python applications. The core implementation resides in behavioral_taint_tracking.py and integrates with the broader analysis pipeline through registry.py and graph.py.

Architecture of the Behavioral Taint-Tracking Analyzer

The analyzer processes each Python file through a multi‑stage pipeline that combines AST parsing with symbolic tracking of tainted variables.

Defining Sources and Sinks

The analyzer maintains two critical sets that define the analysis boundaries: _ALL_SOURCES and _ALL_SINKS. These sets contain fully‑qualified call names representing:

  • Sources: Environment variables (os.getenv), file reads (open), network input (socket.recv), and user input (input)
  • Sinks: Network output (requests.post), code execution (subprocess.run, exec), and file writes (write)

These definitions occupy lines 44–89 and 91–134 in behavioral_taint_tracking.py, establishing the security-relevant endpoints for the data‑flow analysis.

AST Parsing and Type Resolution

For each target file under MAX_FILE_BYTES, the analyzer constructs an abstract syntax tree using Python’s ast.parse (lines 93–99). To resolve call names accurately across imports and aliases, it builds a type map via build_type_map imported from common.py (lines 100–101). This mapping ensures that later stages correctly identify when a tainted source reaches a specific sink, even when accessed through intermediate variables or module aliases.

Taint Introduction and Propagation

Once the AST is prepared, the analyzer walks the tree to identify where untrusted data enters the program and how it moves through variables.

Initial Taint Assignment

When the walker encounters an assignment whose right‑hand side contains a source call, it marks the left‑hand side variable as a _TaintedVar (lines 131–149). The helper _find_source_in_expr detects both direct calls and subscript sources like os.environ["KEY"] (lines 135–138). If the assignment references a previously tainted variable rather than a direct source, _find_tainted_in_expr propagates the taint status to the new variable (lines 162–170).

Propagating Taint Through Data Structures

Taint tracking extends beyond simple variables to include container literals. The analyzer ensures that taint propagates into:

  • Dictionary literals ({"auth": token})
  • List and tuple literals ([secret, config])
  • Set literals
  • f‑strings containing tainted expressions

This propagation logic, implemented in lines 172–182, prevents attackers from bypassing detection by packaging sensitive data into collections before reaching a sink.

Handling Complex Assignment Targets

For assignments involving tuple unpacking or nested targets, the _mark_targets helper (lines 191–199) recursively extracts all target names and registers them as tainted. This ensures that patterns like a, b = source(), other() correctly mark only the relevant variables.

Detecting and Reporting Sink Violations

The final stage determines whether tainted data reaches a security‑critical sink and generates appropriate findings.

Direct vs. Indirect Flow Detection

When encountering a call node matching _ALL_SINKS, the analyzer examines arguments using two complementary strategies:

  • Direct flows: _find_nested_sources detects when a source call is nested directly inside the sink arguments (lines 225–236). For example, requests.post(url, data=os.getenv("KEY")) triggers this check.
  • Indirect flows: _find_tainted_names_in_args identifies when a tainted variable is passed as an argument to a sink (lines 238–250). For example, token = os.getenv("KEY"); requests.post(url, data=token) triggers this path.

Rule Selection and Finding Emission

The _pick_rule method (lines 176–186) maps the specific source‑sink combination to a rule ID (TT1–TT5) with appropriate severity. For instance, a credential source flowing to a network output maps to TT3, while taint reaching an execution sink typically maps to TT1.

The _emit function (lines 210–224) constructs an AnalyzerFinding containing the rule ID, severity, confidence, code location, and descriptive message. A seen set (lines 212–218) deduplicates findings based on location and flow path to prevent redundant reports.

Integration with the Analysis Pipeline

The analyzer’s node() method (lines 292–306) converts internal AnalyzerFinding objects to the generic Finding format defined in models.py, returning them to the framework. The node is registered under "behavioral_taint_tracking" in registry.py and orchestrated by graph.py alongside other analyzers.

Practical Examples

The following examples demonstrate how the analyzer detects different flow patterns.

Direct Source-to-Sink Flow

import os
import requests

api_key = os.getenv("API_KEY")  # Source

requests.post("https://example.com", json={"key": api_key})  # Sink

The analyzer flags a TT3 finding because os.getenv (credential/environment source) flows directly to requests.post (network output).

Indirect Flow via Re-assignment

import os
import subprocess

secret = os.environ["SECRET"]  # Source

payload = secret  # Taint propagates

subprocess.run(["/bin/echo", payload])  # Sink

This generates a TT1 finding, indicating an indirect tainted flow from os.getenv to subprocess.run (code execution).

Taint Through Container Literals

import os
import requests

token = os.getenv("TOKEN")
data = {"auth": token}  # Container propagates taint

requests.post("https://api.service", json=data)

The analyzer traces the tainted dictionary element to the network sink, reporting the flow despite the intermediate data structure.

Summary

  • Source and sink definitions in _ALL_SOURCES and _ALL_SINKS establish the security perimeter for the analysis.
  • AST parsing with ast.parse and build_type_map enables accurate name resolution across Python files.
  • Taint introduction via _find_source_in_expr and _TaintedVar marks entry points, while _find_tainted_in_expr handles propagation.
  • Container awareness ensures taint persists through dict, list, tuple, set, and f‑string constructions.
  • Sink detection distinguishes between direct nested flows and indirect variable flows using _find_nested_sources and _find_tainted_names_in_args.
  • Rule mapping assigns TT1–TT5 identifiers via _pick_rule, with _emit generating deduplicated findings integrated into the SkillSpector pipeline.

Frequently Asked Questions

What constitutes a source or sink in SkillSpector?

Sources are functions that introduce untrusted data into the application, such as os.getenv, os.environ subscripts, file reads, and network input. Sinks are dangerous operations where tainted data could cause harm, including network output (requests.post), code execution (subprocess.run, exec), and file writes. These are defined as sets of fully‑qualified call names in behavioral_taint_tracking.py (lines 44–134).

How does SkillSpector handle taint propagation through Python containers?

The analyzer explicitly tracks taint propagation into dictionary, list, tuple, and set literals during the AST walk (lines 172–182). When a tainted variable is inserted into a container, the entire container or its specific element is marked as tainted, ensuring that subsequent extraction or usage still triggers a finding when reaching a sink.

What is the difference between direct and indirect taint flows?

A direct flow occurs when a source call is nested directly inside a sink call argument, detected by _find_nested_sources (lines 225–236). An indirect flow occurs when a previously tainted variable is passed to a sink, detected by _find_tainted_names_in_args (lines 238–250). Direct flows typically indicate higher confidence and may map to different rule IDs (e.g., TT3 vs TT1) based on the specific source‑sink combination.

How does the analyzer prevent duplicate findings?

The _emit function maintains a seen set (lines 212–218) that tracks previously reported findings based on their location, source, sink, and flow path. Before constructing a new AnalyzerFinding, the checker verifies whether an identical finding was already emitted during the current file analysis, ensuring clean, deduplicated results in the final report.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →