Aug 29, 2026·6 min read·23 visits
Incomplete validation of positional-only arguments in RestrictedPython allowed attackers to shadow security hooks and escape the sandbox.
A critical security flaw was identified in RestrictedPython prior to version 8.3 where positional-only arguments introduced in Python 3.8 were not properly validated. This allowed an attacker executing code within the sandbox to shadow critical security guards like `_write_` and `_getattr_`, leading to a complete sandbox escape and arbitrary code execution on the underlying server.
RestrictedPython is a specialized tool designed to define a safe subset of the Python language. It is commonly integrated into frameworks such as Zope and Plone to allow the execution of untrusted, user-supplied code in a restricted execution environment. To achieve this, RestrictedPython parses source code into an Abstract Syntax Tree (AST) and rewrites potentially hazardous operations into safe, validation-wrapped calls.
This rewritten AST relies on security guard hooks such as _getattr_, _getitem_, and _write_ to validate attribute access, subscripts, and mutation operations at runtime. These hooks are dynamically injected into the restricted program's global execution namespace. The security model relies entirely on preventing the sandboxed code from modifying or shadowing these protected identifiers, which are reserved and prefixed with leading underscores.
Prior to version 8.3, a structural omission existed in RestrictedPython's AST validation parser. When defining function parameter names, the validation engine failed to inspect positional-only arguments introduced in Python 3.8. This failure allows an attacker to bypass name-checking restrictions, leading to complete sandbox evasion and potential arbitrary code execution.
The root cause of this vulnerability lies in the Abstract Syntax Tree (AST) translation phase, specifically within the check_function_argument_names method inside src/RestrictedPython/transformer.py. This method is responsible for validating all parameter names in function and lambda definitions. Its purpose is to ensure that no parameter shadows the injected security hooks by starting with an underscore, such as _write_ or _getattr_.
In Python 3.8, PEP 570 introduced positional-only parameters, which are separated from other arguments by a forward slash (/). To support this syntax, the Python AST was updated to store these parameters in a distinct list field named posonlyargs under the function arguments node. However, the AST transformer in RestrictedPython was not updated to iterate through this newly introduced list.
Consequently, the validation loop inside check_function_argument_names checked standard arguments (args), keyword-only arguments (kwonlyargs), variable arguments (vararg), and keyword arguments (kwarg), but entirely omitted posonlyargs. This omission allowed positional-only arguments named after security hooks to pass compilation without throwing an error.
A review of the vulnerable implementation in src/RestrictedPython/transformer.py highlights the missing check for the posonlyargs collection.
# Vulnerable implementation in RestrictedPython/transformer.py
def check_function_argument_names(self, node):
# The posonlyargs collection is completely ignored here
for arg in node.args.args:
self.check_name(node, arg.arg)
for arg in node.args.kwonlyargs:
self.check_name(node, arg.arg)
if node.args.vararg is not None:
self.check_name(node, node.args.vararg.arg)
if node.args.kwarg is not None:
self.check_name(node, node.args.kwarg.arg)The patch applied in commit 3737596ec9f28c34a073cc845bd2f4c0a80cb671 addresses this gap by prepending a loop that checks node.args.posonlyargs before verifying the remaining argument types.
# Patched implementation in RestrictedPython/transformer.py
def check_function_argument_names(self, node):
# Added check to ensure positional-only parameters are validated
for arg in node.args.posonlyargs:
self.check_name(node, arg.arg)
for arg in node.args.args:
self.check_name(node, arg.arg)
for arg in node.args.kwonlyargs:
self.check_name(node, arg.arg)
if node.args.vararg is not None:
self.check_name(node, node.args.vararg.arg)
if node.args.kwarg is not None:
self.check_name(node, node.args.kwarg.arg)This simple addition closes the validation gap. Any attempt to use restricted identifiers in positional-only parameters will trigger a compilation error, maintaining the integrity of the sandbox's execution environment.
Exploitation of CVE-2026-55830 requires the capability to compile and execute custom Python scripts within the restricted environment. Because the compiler fails to check positional-only arguments, an attacker can define a function or lambda where a positional-only argument is named exactly after a protected global guard, such as _write_.
# Shadowing the global write guard hook
def escape_sandbox(_write_=lambda obj: obj, /):
# Inside this scope, attribute write operations use the local _write_
passWhen Python compiles this function, it generates bytecode that resolves _write_ using the local scope rather than the global environment. At runtime, the local parameter _write_ is initialized with the attacker-controlled default lambda value. Any attribute write operation executed within the scope of this function is redirected through this lambda, which bypasses the validation and returns the raw target object.
An attacker can use this bypassed validation to access and modify the execution environment's internal namespace. By overriding the global dictionary's _getattr_ and _write_ hooks, the attacker disables the security boundary completely. This allows the script to import standard modules such as os or subprocess and execute arbitrary system commands on the host machine.
The security impact of CVE-2026-55830 is classified as High, with a CVSS v3.1 base score of 8.3. The vulnerability represents a complete breakdown of the isolation boundary provided by RestrictedPython, allowing sandboxed code to escape and achieve execution in the context of the parent application process.
An attacker who successfully escapes the sandbox can read and write arbitrary files, compromise system configurations, and access database credentials or environment variables stored on the host. If the parent application is running with elevated privileges, the entire hosting environment can be compromised.
While the vulnerability requires high privileges to submit and run scripts, the exploitation complexity is low because the bypass works reliably without relying on race conditions or system state. The scope change metric is set to 'Changed' because the impact extends from the restricted interpreter to the host operating system.
The recommended remediation for this vulnerability is to upgrade RestrictedPython to version 8.3 or higher. If immediate upgrading is not possible, defensive mitigations must be implemented to inspect user-provided scripts prior to passing them to the RestrictedPython compiler.
Organizations can deploy an AST pre-filter utility that parses the incoming source code and rejects any structure containing positional-only arguments that begin with an underscore. This can be integrated into the application's intake validation pipelines.
import ast
def validate_ast_safety(source: str) -> bool:
try:
tree = ast.parse(source)
except SyntaxError:
return False
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
for arg in node.args.posonlyargs:
if arg.arg.startswith('_'):
return False
return TrueAdditionally, applications should employ strong process isolation. Running the python process within a containerized environment with minimal privileges and strict network access controls can limit the post-exploitation damage of a sandbox escape.
CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:H/A:L| Attribute | Detail |
|---|---|
| CWE ID | CWE-184 |
| Attack Vector | Network |
| CVSS v3.1 | 8.3 (High) |
| EPSS Score | 0.00401 (Percentile: 33.04%) |
| Exploit Status | Proof of Concept (PoC) |
| CISA KEV Status | Not Listed |
| Impact | Arbitrary Code Execution (ACE) via Sandbox Escape |
CVE-2026-109619 (PHP2Shell) is a critical Use-After-Free vulnerability in the core PHP engine caused by improper global state cleanup during out-of-memory exception handling across persistent worker requests.
In Python virtualenv prior to version 21.14.9, activation script generators and configuration writers use Path.exists() before writing files. Because Path.exists() resolves symbolic links, it returns False for dangling symlinks. Consequently, virtualenv skips unlinking pre-planted symbolic links or hard links, writing data directly to target locations outside the intended virtual environment directory.
A remote code execution vulnerability exists in Microsoft .NET due to untrusted data deserialization within the PresentationCore component. The flaw stems from implicit auto-conversion flags during clipboard data retrieval operations.
An out-of-bounds write vulnerability in Microsoft .NET Framework, .NET runtime, and Visual Studio allows unauthenticated remote attackers to execute arbitrary code via crafted pixel-shader bytecode or auto-converted clipboard payloads.
ASP.NET Core contains a thread deadlock vulnerability (CWE-833) that allows an unauthenticated remote attacker to cause a denial of service over the network by exhausting server thread pool resources.
CVE-2026-70035 is a high-severity Remote Code Execution (RCE) vulnerability in Microsoft .NET Framework, .NET runtime, and Visual Studio caused by improper handling during the deserialization of untrusted data (CWE-502). An unauthenticated attacker can execute arbitrary code within the context of the affected process by delivering a crafted serialized payload.