← Community
bugopen

module_sandbox subprocess:false is namespace-only — import subprocess in a handler gets the real module and spawns

ShwetaShweta#112d ago · 27 views
affected: station-v0.99

Reproduction steps:

  1. Build the namespace and install the sandbox a module opted into:

ns = {"os": __import__("os"), "json": __import__("json"), "time": __import__("time")}
module_sandbox.install_restrictions(ns, {"subprocess": False}, "evil/mod")
(Modules tab shows subprocess as blocked.)

  1. Control — the bare injected name is blocked:

exec("def h():\n return subprocess.run(['echo','x'])", ns); ns["h"]() # SandboxViolation

  1. Import the module the ordinary way and spawn:

exec("def h():\n import subprocess\n return subprocess.check_output(['echo','ESCAPED'])", dict(ns))
# also: import os; os.popen('...').read() and __import__('subprocess')

Expected:
A module that declared subprocess:false cannot spawn a process by any standard
means.

Actual:
Only the bare injected name is blocked. import subprocess, `import os;
os.popen(...) and __import__('subprocess')` each fetch the real module from
sys.modules and spawn freely, because the gate only replaces ns["subprocess"]/
ns["os"] with refusing proxies and never patches the real modules. The sibling
network gate in the same file DOES patch socket/urllib process-wide (so `import
socket` inside a handler still hits it); the subprocess gate does not, so the two
give different real guarantees for the same requires mechanism.

Suggested fix:
Patch the spawn primitives process-wide, gated by the same _ACTIVE_SANDBOX
contextvar the network gate already uses (so Studio's own subprocess use is
untouched), instead of only shadowing ns entries:

import subprocess as _real_sp, os as _real_os
_real_popen_init = _real_sp.Popen.__init__
def _guarded_popen_init(self, *a, **k):
act = _ACTIVE_SANDBOX.get()
if act is not None:
raise SandboxViolation(f"module {act[0]!r} tried subprocess — subprocess:false")
return _real_popen_init(self, *a, **k)
_real_sp.Popen.__init__ = _guarded_popen_init # run/call/check_ all build a Popen
for _n in ("system", "popen"):
_orig = getattr(_real_os, _n)
def _mk(orig, n):
def _w(
a, **k):
if _ACTIVE_SANDBOX.get() is not None:
raise SandboxViolation(f"module tried os.{n} — subprocess:false")
return orig(*a, **k)
return _w
setattr(_real_os, _n, _mk(_orig, _n))
# also os.exec*/posix_spawn as the existing proxy already lists

This makes subprocess:false hold against import subprocess the same way the
network gate holds against import socket. The language-layer bypass the module
discloses (ctypes -> libc.system) remains and needs OS-level isolation.

Station version (railcall version): station-v0.99
Module slug + version: N/A (platform — workbench/module_sandbox.py)

0 replies

Sign in to reply.