Studio sandbox: close second-round review bypasses (classifier + backstop)

Fixes a further batch of P1 bypasses and analysis-time DoS vectors found in review.

Static classifier:
- Decode exec/compile bytes payloads the way CPython does (PEP 263 coding cookie
  via tokenize.detect_encoding), then analyze the real source. A bytes payload whose
  UTF-8 view is pure comments but whose utf-7 decode runs hidden code no longer slips
  through; a payload decoding to a blocked op blocks, a benign one stays allowed.
- Resolve exec-builtin aliases assigned in nested scopes (def f(): e = exec; e(...)),
  matching the shell-sink aliasing (stored-once guard keeps it low false-positive).
- Treat deserializer modules (pickle/marshal/dill/...) as dangerous dynamic-import
  targets so __import__('pickle').loads(blob) is caught.
- Flag vars(os) / vars(__builtins__) as a module-__dict__ obfuscation, like os.__dict__.
- Inspect the pathlib receiver path for read methods: Path('../../.ssh/id_rsa').read_text()
  / read_bytes() / open() now check the constructor path, not only call args.
- Fold literal os.path.join / posixpath.join so open(os.path.join('/etc','passwd')).read()
  is seen by the sensitive-read scanner instead of treated as opaque.

Constant-folder allocation DoS (folding runs in-process, before subprocess rlimits):
- Reject oversized f-string / str.format / %-format widths and precisions before
  format() allocates the padded string.
- Reject oversized str padding-method widths (ljust/rjust/center/zfill).
- Cap list/tuple repetition (seq * n) as str/bytes repetition already was.

Runtime realpath backstop:
- Do not publish __wrapped__ on the guard wrappers (functools.wraps would expose the
  original unguarded callable, e.g. open.__wrapped__(outside, 'w')).
- Guard the low-level _io.open entry point (io.open / builtins.open originate there).
- Confine os.chdir to the workdir and deny os.fchdir so a cwd escape cannot turn a
  later relative read/write into a host-path access.
- Deny fd-based metadata mutators (os.fchmod / os.fchown) that could reuse a read-only
  descriptor opened on an outside file.

Adds regression tests across the const-fold, aliasing, exec-recursion and runtime-
backstop suites for every item above.
This commit is contained in:
danielhanchen 2026-07-09 15:17:37 +00:00
commit fbb030b019
5 changed files with 478 additions and 35 deletions

View file

@ -61,6 +61,12 @@ class TestFuncLocalAliasBlocked:
_ok("def run():\n f = sorted\n return f([3, 1, 2])\nrun()")
_ok("import os\ndef run():\n s = os.system\n s('echo done')\nrun()")
def test_func_local_exec_alias_blocked(self):
# An exec builtin aliased inside a function must still be unwrapped and its
# recovered payload analyzed (exec-env aliasing walks the whole tree).
_blocked("def f():\n e = exec\n e(\"__import__('os').system('id')\")\nf()")
_blocked("def f():\n r = eval\n r(\"__import__('os').system('rm -rf /')\")\nf()")
class TestAliasingLowFalsePositive:
def test_reassigned_alias_not_treated_as_sink(self):

View file

@ -71,6 +71,55 @@ class TestConstFoldArithAndConcat:
assert _fold("bytes(10 ** 9)") is None
class TestConstFoldAllocationDoS:
"""Oversized format widths / sequence repetitions must refuse BEFORE the folder
allocates the result (folding runs in the Studio process, ahead of subprocess
rlimits)."""
def test_fstring_width_refused(self):
assert _fold("f'{1:1000000000}'") is None
def test_str_format_width_refused(self):
assert _fold("'{:1000000000}'.format(1)") is None
def test_percent_format_width_refused(self):
assert _fold("'%1000000000d' % 1") is None
def test_pad_method_width_refused(self):
assert _fold("'x'.ljust(1000000000)") is None
assert _fold("'x'.rjust(10 ** 9)") is None
assert _fold("'x'.center(2000000000)") is None
assert _fold("'x'.zfill(10 ** 9)") is None
def test_list_tuple_repeat_refused(self):
assert _fold("[0] * 1000000000") is None
assert _fold("(1,) * 10 ** 9") is None
def test_benign_format_and_repeat_still_fold(self):
assert _fold("f'{2 + 2}'") == "4"
assert _fold("'{:>8}'.format('hi')") == " hi"
assert _fold("'%05d' % 7") == "00007"
assert _fold("'x'.ljust(10)") == "x "
assert _fold("[0] * 8") == [0] * 8
class TestConstFoldPathJoin:
"""os.path.join / posixpath.join of string literals fold so the sensitive-read
scanner sees the concrete path (628)."""
def test_os_path_join_literal(self):
assert _fold("os.path.join('/etc', 'passwd')") == "/etc/passwd"
def test_posixpath_join_literal(self):
assert _fold("posixpath.join('/etc', 'shadow')") == "/etc/shadow"
def test_relative_join_literal(self):
assert _fold("os.path.join('sub', 'a.txt')") == "sub/a.txt"
def test_join_nonliteral_unknown(self):
assert _fold("os.path.join('/etc', x)") is None
class TestConstFoldJoinFormatFstring:
def test_sep_join(self):
assert _fold('".".join(["os", "system"])') == "os.system"

View file

@ -289,6 +289,86 @@ def test_inject_sandbox_guard_plain_prepend_without_future():
assert _inject_sandbox_guard(code, prelude) == prelude + code
@_POSIX_ONLY
def test_sandboxed_open_wrapped_attr_removed(tmp_path):
# functools.wraps would publish the ORIGINAL unguarded callable on __wrapped__;
# the guard must not expose it (open.__wrapped__(outside, 'w') would bypass).
target = tmp_path / "wrapped_escape.txt"
out = _python_exec(
f"open.__wrapped__({str(target)!r}, 'w').write('x'); print('WROTE')",
None,
30,
"backstop-wrapped",
disable_sandbox = False,
)
assert not target.exists()
assert "AttributeError" in out or "sandbox:" in out
@_POSIX_ONLY
def test_sandboxed_low_level_io_open_denied(tmp_path):
# io.open / builtins.open originate from the C module _io; patching io.open leaves
# _io.open untouched, so it must be guarded too.
target = tmp_path / "lowio_escape.txt"
out = _python_exec(
f"import _io; _io.open({str(target)!r}, 'w').write('x'); print('WROTE')",
None,
30,
"backstop-lowio",
disable_sandbox = False,
)
assert "sandbox:" in out
assert not target.exists()
@_POSIX_ONLY
def test_sandboxed_chdir_escape_denied():
# os.chdir outside the workdir would let a later relative read/write (which the
# static scan treats as local) escape, so cwd changes are confined.
out = _python_exec(
"import os\nos.chdir('/etc')\nprint('CWD', os.getcwd())",
None,
30,
"backstop-chdir",
disable_sandbox = False,
)
assert "sandbox:" in out and "chdir" in out
@_POSIX_ONLY
def test_sandboxed_chdir_within_workdir_allowed():
out = _python_exec(
"import os\nos.chdir('.')\nprint('CWD-OK')",
None,
30,
"backstop-chdir-ok",
disable_sandbox = False,
)
assert "CWD-OK" in out
assert "sandbox:" not in out
@_POSIX_ONLY
def test_sandboxed_fd_metadata_mutator_denied(tmp_path):
# A read-only os.open of an outside file is allowed (reads are not confined), but
# fd-based metadata mutators (os.fchmod/fchown) must be denied so they cannot be
# reused to mutate host files.
victim = tmp_path / "victim.txt"
victim.write_text("x")
os.chmod(victim, 0o600)
out = _python_exec(
"import os\n"
f"fd = os.open({str(victim)!r}, os.O_RDONLY)\n"
"os.fchmod(fd, 0o644); print('CHMODDED')",
None,
30,
"backstop-fchmod",
disable_sandbox = False,
)
assert "sandbox:" in out and "fchmod" in out
assert oct(os.stat(victim).st_mode & 0o777) == "0o600"
@_POSIX_ONLY
def test_sandboxed_imports_still_work_under_guard():
# The guard must not break library imports (bytecode caching failures are

View file

@ -874,6 +874,47 @@ class TestAliasIntrospectionBypasses:
assert _check_code_safety(code) is not None, code
class TestReceiverAndVarsAndDynImportBypasses:
"""Second-round bypasses: sensitive reach through a pathlib receiver, vars() on a
module, and dynamic import of a deserializer module."""
@pytest.mark.parametrize(
"code",
[
# 572: sensitive path on the pathlib receiver, not in a call arg.
"from pathlib import Path\nPath('../../.ssh/id_rsa').read_text()",
"from pathlib import Path\nPath('/etc/passwd').read_bytes()",
"from pathlib import Path\nPath('/etc/passwd').open().read()",
# 617: vars(module) exposes the module __dict__.
"import os\nvars(os)['system']('rm -rf /')",
"vars(__builtins__)['eval']('x')",
# 596: dynamic import of a deserializer module runs a reduce payload.
"__import__('pickle').loads(blob)",
"__import__('marshal').loads(b)",
"import importlib\nimportlib.import_module('pickle').loads(b)",
# 628: literal os.path.join to a host secret.
"import os\nopen(os.path.join('/etc', 'passwd')).read()",
],
)
def test_blocked(self, code):
assert _check_code_safety(code) is not None, code
@pytest.mark.parametrize(
"code",
[
"from pathlib import Path\nPath('data/out.txt').read_text()",
"from pathlib import Path\nPath('model.json').open()",
"vars(obj)",
"vars()",
"import pickle\npickle.dumps(x)",
"import importlib\nimportlib.import_module('numpy')",
"import os\nopen(os.path.join('sub', 'a.txt'))",
],
)
def test_benign_allowed(self, code):
assert _check_code_safety(code) is None, code
class TestEvalExecRecursion:
"""Stage 2: eval/exec/compile are unwrapped, not blanket-banned. A safe
(constant-recoverable) payload is allowed; an obfuscated escape blocks."""
@ -946,27 +987,40 @@ class TestEvalExecRecursion:
def test_import_concat_benign_module_allowed(self):
_ok('__import__("hugging" + "face_hub")')
def test_exec_utf7_bytes_coding_cookie_blocked(self):
# exec()/eval()/compile() honor PEP 263 coding cookies on *bytes*: a UTF-7
# payload behind "# coding: utf-7" decodes to real Python that the UTF-8
# static view (which is SYNTAX_BAD) never sees. An unparseable *bytes*
# payload for an executing sink must block.
payload = (
b"# coding: utf-7\n"
b"+AGkAbQBwAG8AcgB0ACAAbwBz-\n"
b"+AG8AcwAuAHMAeQBzAHQAZQBtACgAJwBpAGQAJwAp-"
)
# self-check: the cookie-decoded payload really is os.system ACE.
assert "os.system" in payload.decode("utf-7")
def test_exec_utf7_comment_cookie_smuggle_blocked(self):
# The exec/eval/compile sinks honor a PEP 263 coding cookie on *bytes*. Here
# the UTF-8 view is TWO comment lines (safe), but "+AAo-" decodes (UTF-7) to a
# newline, so exec(bytes) actually runs the hidden __import__('os') call. The
# analyzer must decode with the cookie's codec, not read the UTF-8 view.
sneaky = b"# coding: utf_7\n#+AAo-__import__('os').system('id')\n"
# self-check: UTF-8 view is pure comments; the cookie decode reveals the call.
import ast as _ast
_ast.parse(sneaky.decode("utf-8")) # parses (comments only) under UTF-8
assert "__import__('os')" in sneaky.decode("utf-7")
for sink in ("exec(%r)", "exec(compile(%r, '<s>', 'exec'))"):
assert _check_code_safety(sink % sneaky) is not None, sink
def test_exec_utf7_bytes_decodes_to_blocked_op(self):
# A bytes payload behind a coding cookie whose decoded source reaches a blocked
# operation must block for every executing sink (eval sees a statement -> the
# SYNTAX_BAD-bytes backstop still trips).
payload = b"# coding: utf-7\n" + "import os\nos.system('rm -rf /')\n".encode("utf-7")
assert "rm -rf" in payload.decode("utf-7")
for sink in ("exec(%r)", "eval(%r)", "exec(compile(%r, '<s>', 'exec'))"):
code = sink % payload
assert _check_code_safety(code) is not None, code
# bare compile() does not run, so it stays allowed (the exec of its result
# is where the block lands).
assert _check_code_safety("compile(%r, '<s>', 'exec')" % payload) is None
assert _check_code_safety(sink % payload) is not None, sink
def test_exec_plain_bytes_payload_allowed(self):
# Legitimate exec/eval of ASCII/UTF-8 bytes that parse cleanly stay allowed.
_ok('exec(b"x = 1")')
_ok('exec(b"print(1)")')
_ok('eval(b"2 + 2")')
# A UTF-7 payload that decodes to a benign, non-blocked call stays allowed too
# (os.system('id') is benign -- 'id' is not a blocked command), matching the
# plain-text exec("import os; os.system('id')") behavior.
benign = (
b"# coding: utf-7\n"
b"+AGkAbQBwAG8AcgB0ACAAbwBz-\n"
b"+AG8AcwAuAHMAeQBzAHQAZQBtACgAJwBpAGQAJwAp-"
)
_ok("exec(%r)" % benign)