Harden sandbox: $VAR PATH, hash -p, find -fls, git output/env-C/hooks, xargs --arg-file, watch, numpy allow_pickle, yaml positional loader

Close nine static-classifier bypasses and one false positive from Codex, plus
neutralize git hooks in the sandbox env:

- $VAR-expanded PATH: a PATH component from a shell variable bound to a relative
  / cwd value (P=.; PATH=$P evil) or a relative ${VAR:-.} default resolved to the
  workdir but was treated as a trusted absolute path. _path_value_is_unsafe now
  brace-aware splits the list and resolves local VAR=value bindings and ${VAR-def}
  defaults; $PATH / an unknown external $VAR (a trusted absolute) stays allowed.
- hash -p: hash -p PATHNAME NAME binds a command name to PATHNAME, so a later bare
  NAME runs a local executable unguarded (hash -p ./evil ls; ls). Block hash -p
  with a local-executable pathname.
- find -fls: -fls FILE writes its listing to FILE like -fprint/-fprintf; add it to
  the mutating-find actions.
- git output options: --output / -o / --output-directory (git archive / format-
  patch) carry an inline path git writes to; a value escaping the workdir is now
  flagged alongside -C / --git-dir / --work-tree / --separate-git-dir.
- env -C git: env -C DIR / --chdir DIR changes git's cwd, so a bare or relative
  git write subcommand (env -C /tmp git init) resolves under DIR. The git scan now
  looks back for an escaping env -C wrapper. A relative env -C sub, and env -C with
  a non-git reader, stay allowed.
- xargs --arg-file: xargs -a FILE / --arg-file[=]FILE reads its argument list FROM
  FILE, so a sensitive / expanded target is a host-file read even though xargs is a
  wrapper; the read scanner now flags it.
- watch: watch [options] command runs command (via sh -c or exec -x); add watch as
  a command prefix with its -n operand so the wrapped writer is resolved.
- numpy allow_pickle: numpy.load unpickles when allow_pickle is truthy; a non-
  literal (flag=True) or splatted (**{'allow_pickle': True} / **kw) value is now
  rejected. allow_pickle absent / a constant False stays allowed.
- yaml positional loader (FALSE POSITIVE fix): yaml.load(data, yaml.SafeLoader)
  passes the loader positionally; _yaml_call_has_safe_loader now accepts args[1],
  so the safe positional form is no longer wrongly blocked.
- git hooks: git runs repository hooks (.git/hooks/*) in an unguarded child; a
  sandboxed snippet could plant one and trigger it via git commit / merge /
  checkout. _build_safe_env points core.hooksPath at a non-directory (via git's
  env-config mechanism) so no repository hook runs for any git subcommand, without
  blocking git itself.

Regression coverage: TestRound36Bypasses in tests/test_sandbox_tools.py and the
sandbox-env whitelist test.
This commit is contained in:
danielhanchen 2026-07-10 11:52:31 +00:00
commit c3be77614b
2 changed files with 311 additions and 34 deletions

View file

@ -286,30 +286,90 @@ def _is_local_executable_path(tok: str) -> bool:
return not norm.startswith(_SYSTEM_BIN_PREFIXES)
def _path_value_is_unsafe(value: str) -> bool:
def _split_path_entries(value: str):
"""Split a PATH value on ':' separators, but NOT on a ':' inside a ${...} expansion (so a
${VAR:-default} default operator is not mistaken for a list separator)."""
entries = []
cur = []
depth = 0
i = 0
v = value.replace("\\", "/")
while i < len(v):
c = v[i]
if c == "$" and i + 1 < len(v) and v[i + 1] == "{":
depth += 1
cur.append("${")
i += 2
continue
if c == "}" and depth > 0:
depth -= 1
cur.append(c)
i += 1
continue
if c == ":" and depth == 0:
entries.append("".join(cur))
cur = []
i += 1
continue
cur.append(c)
i += 1
entries.append("".join(cur))
return entries
def _path_var_resolves_unsafe(var, assignments):
"""Whether a PATH component expanded from shell variable ``var`` can resolve to the workdir.
HOME / PWD are the session workdir; PATH is the trusted search list; a var assigned a
relative / cwd value earlier in the same command (P=.; PATH=$P) is unsafe; an unknown
external var (CONDA_PREFIX) is assumed to expand to a trusted absolute path."""
if var in ("HOME", "PWD"):
return True
if var == "PATH":
return False
if assignments and var in assignments:
return _path_value_is_unsafe(assignments[var], assignments)
return False
def _path_value_is_unsafe(value: str, assignments = None) -> bool:
"""True when a PATH search list would let a BARE (no-slash) command resolve to a workdir
executable: any entry that is ``.``, empty (``:`` = cwd), a relative directory, or one that
expands to the session workdir. In the sandbox ``HOME`` and the child cwd ARE the workdir,
so ``~`` / ``~user`` and ``$HOME`` / ``$PWD`` (``${HOME}`` / ``${PWD}``) are unsafe. An
absolute (``/...``), ``%VAR%``, or other ``$VAR`` entry (``$PATH``, ``$CONDA_PREFIX/bin``,
assumed to expand to a trusted absolute path) is safe. Used so a ``PATH=. evil`` /
``PATH=~/bin evil`` prefix / ``env={'PATH': '~/bin'}`` cannot smuggle an unguarded shebang
past the bare-name PATH exemption in _is_local_executable_path."""
for entry in value.replace("\\", "/").split(":"):
so ``~`` / ``~user``, ``$HOME`` / ``$PWD``, a ``${VAR:-.}`` default that is relative, and a
``$VAR`` bound to a relative value earlier in the same command (``P=.; PATH=$P``) are unsafe.
An absolute (``/...``), ``%VAR%``, or unknown external ``$VAR`` entry (``$PATH``,
``$CONDA_PREFIX/bin``, assumed to expand to a trusted absolute path) is safe. ``assignments``
maps local shell VAR=value bindings so a locally-controlled expansion can be resolved."""
for entry in _split_path_entries(value):
e = entry.strip()
if e in ("", "."):
return True
# ~ / ~user expand to HOME, which is the session workdir in the sandbox.
if e.startswith("~"):
return True
if e.startswith("${"):
inner = e[2:]
if inner.endswith("}"):
inner = inner[:-1]
# ${VAR-def} / ${VAR:-def} / ${VAR=def} / ${VAR:=def}: def applies when VAR is unset/
# empty, so a relative default is unsafe. ${VAR:+alt} / ${VAR:?msg} carry no path.
m = re.match(r"([A-Za-z_][A-Za-z0-9_]*)(:?[-=?+])(.*)$", inner)
if m:
var, op, default = m.group(1), m.group(2), m.group(3)
if op in (":-", "-", ":=", "=") and _path_value_is_unsafe(default, assignments):
return True
if _path_var_resolves_unsafe(var, assignments):
return True
continue
var = re.split(r"[/}]", inner, maxsplit = 1)[0]
if _path_var_resolves_unsafe(var, assignments):
return True
continue
if e.startswith("$"):
name = e[1:]
if name.startswith("{"):
name = name[1:]
name = re.split(r"[/}]", name, maxsplit = 1)[0]
if name in ("HOME", "PWD"):
return True # expands to the session workdir
continue # $PATH / other vars: assume a trusted absolute expansion
m = re.match(r"\$([A-Za-z_][A-Za-z0-9_]*)", e)
if m and _path_var_resolves_unsafe(m.group(1), assignments):
return True
continue # $PATH / $CONDA_PREFIX / $1: assume a trusted absolute expansion
if e.startswith(("/", "%")):
continue
return True # a relative directory (relbin, ./tools)
@ -328,11 +388,23 @@ def _arg_escapes_workdir(tok: str) -> bool:
return ".." in t.split("/")
# git subcommands / global options that CREATE files or repositories at an arbitrary path in an
# unguarded child (the runtime realpath backstop never sees a native git process). A path
# operand or -C / --git-dir / --work-tree / --separate-git-dir value that escapes the workdir
# lets git write outside the session (git init /tmp/x, git clone url /tmp/x, git -C /outside ...).
_GIT_DIR_OPTIONS = frozenset({"--git-dir", "--work-tree", "--separate-git-dir"})
# git options whose VALUE is a path that a native git child writes to / operates in (the runtime
# realpath backstop never sees a native git process). A value that escapes the workdir lets git
# write outside the session: -C / --git-dir / --work-tree / --separate-git-dir (repo location),
# and -o / --output / -O / --output-directory (git archive / format-patch write their output
# file there). Handled for `-x val`, `--opt val`, and inline `--opt=val` forms.
_GIT_PATH_VALUE_OPTIONS = frozenset(
{
"-C",
"--git-dir",
"--work-tree",
"--separate-git-dir",
"-o",
"--output",
"-O",
"--output-directory",
}
)
# The only shell redirection targets trusted without a realpath check: standard device
@ -407,6 +479,9 @@ _COMMAND_PREFIXES = frozenset(
# chrt [options] <priority> <command> [<arg>...]: util-linux scheduler wrapper that
# execs the following command, so chrt -o 0 touch /tmp/x must resolve to touch.
"chrt",
# watch [options] command: repeatedly runs command (via sh -c, or exec with -x), so
# watch -x touch /tmp/x / watch -n 2 rm -rf / must resolve to the wrapped command.
"watch",
}
)
_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
@ -468,6 +543,7 @@ _WRAPPER_OPERAND_FLAGS = {
),
"time": frozenset({"-f", "--format", "-o", "--output"}),
"chrt": frozenset({"-T", "--sched-runtime", "-P", "--sched-period", "-D", "--sched-deadline"}),
"watch": frozenset({"-n", "--interval"}),
}
@ -1288,6 +1364,14 @@ def _find_blocked_commands(command: str) -> set[str]:
if _an in ("BASH_ENV", "ENV") and _av != "":
blocked.add("shell-startup-env:" + _an)
# Local VAR=value bindings in this command, so a PATH component expanded from a locally-set
# variable (P=.; PATH=$P evil) can be resolved to its (unsafe) value.
_local_assigns = {}
for _et in tokens:
if _ASSIGNMENT_RE.match(_et):
_n, _, _v = _et.partition("=")
_local_assigns[_n] = _v
# A BASH_ENV / ENV assignment set in a SEPARATE command (export BASH_ENV=env.sh; bash -c
# '...', or a standalone BASH_ENV=env.sh) persists for later shells in the same session and
# is sourced before their -c payload, so the per-shell prefix backscan above misses it.
@ -1301,7 +1385,7 @@ def _find_blocked_commands(command: str) -> set[str]:
# PATH=. cmd / export PATH=.:$PATH: a search list with a relative / cwd entry lets a bare
# command word resolve to a workdir shebang (the bare-name PATH exemption assumes a
# trusted PATH). Flag an unsafe PATH assignment.
elif _an == "PATH" and _path_value_is_unsafe(_av):
elif _an == "PATH" and _path_value_is_unsafe(_av, _local_assigns):
blocked.add("unsafe-path-assign")
# git -c alias.X='!CMD' X / git config alias.X '!CMD': a git alias whose value starts with
@ -1310,6 +1394,21 @@ def _find_blocked_commands(command: str) -> set[str]:
for i in _cmd_word_idx:
if _token_basename(tokens[i]) != "git":
continue
# An env -C DIR / --chdir DIR wrapper BEFORE git changes git's cwd, so even a bare or
# relative write subcommand (env -C /tmp git init) resolves under DIR. Scan back to the
# previous separator for such a wrapper; if DIR escapes the workdir, git operates outside.
_git_cwd_escapes = False
for _bk in range(i - 1, -1, -1):
_bt = tokens[_bk]
if _bt in _SHELL_SEPARATORS or _bt in _SHELL_KEYWORDS_AS_SEP:
break
if _bt in ("-C", "--chdir") and _bk + 1 < len(tokens):
if _arg_escapes_workdir(tokens[_bk + 1]):
_git_cwd_escapes = True
elif _bt.startswith("--chdir=") and _arg_escapes_workdir(_bt.split("=", 1)[1]):
_git_cwd_escapes = True
if _git_cwd_escapes:
blocked.add("git-write-outside")
_seg = []
for k in range(i + 1, len(tokens)):
if tokens[k] in _SHELL_SEPARATORS or tokens[k] in _SHELL_KEYWORDS_AS_SEP:
@ -1331,18 +1430,13 @@ def _find_blocked_commands(command: str) -> set[str]:
_gk = 0
while _gk < len(_seg):
_gt = _seg[_gk]
if _gt == "-C" and _gk + 1 < len(_seg):
if _arg_escapes_workdir(_seg[_gk + 1]):
blocked.add("git-write-outside")
_gk += 2
continue
if _gt in _GIT_DIR_OPTIONS and _gk + 1 < len(_seg):
if _gt in _GIT_PATH_VALUE_OPTIONS and _gk + 1 < len(_seg):
if _arg_escapes_workdir(_seg[_gk + 1]):
blocked.add("git-write-outside")
_gk += 2
continue
_oeq = None
for _opt in _GIT_DIR_OPTIONS:
for _opt in _GIT_PATH_VALUE_OPTIONS:
if _gt.startswith(_opt + "="):
_oeq = _gt.split("=", 1)[1]
break
@ -1353,6 +1447,21 @@ def _find_blocked_commands(command: str) -> set[str]:
blocked.add("git-write-outside")
_gk += 1
# hash -p PATHNAME NAME binds the command NAME to PATHNAME in the shell's hash table, so a
# later bare `NAME` runs PATHNAME. With a local executable (hash -p ./evil ls; ls) that
# launches an unguarded workdir shebang under a benign-looking command word. Block hash -p
# when its pathname operand is a local executable path.
for i in _cmd_word_idx:
if _token_basename(tokens[i]) != "hash":
continue
for k in range(i + 1, len(tokens)):
t = tokens[k]
if t in _SHELL_SEPARATORS or t in _SHELL_KEYWORDS_AS_SEP:
break
if t == "-p" and k + 1 < len(tokens) and _is_local_executable_path(tokens[k + 1]):
blocked.add("hash-p-local-exec")
break
# alias x='touch /tmp/p'; ...; x (with expand_aliases) runs the alias BODY at execution
# time, but the command word `x` is unknown to the scanner. Scan the body of each alias
# definition so a blocked writer / interpreter in it is caught at the definition site.
@ -1491,7 +1600,9 @@ def _find_blocked_commands(command: str) -> set[str]:
blocked.add("mutating:sort")
break
elif _base == "find":
if al == "-delete" or al.startswith("-fprint"):
# -delete removes; -fprint/-fprintf/-fprint0 and -fls write their listing to a
# named FILE (find . -fls /tmp/escape truncates/creates it in an unguarded child).
if al == "-delete" or al.startswith("-fprint") or al == "-fls":
blocked.add("mutating:find")
break
elif _base == "dd":
@ -1636,6 +1747,14 @@ def _build_safe_env(workdir: str) -> dict[str, str]:
# startup. Disable the per-user site directory here too (belt-and-suspenders with the
# interpreter's -s flag) so a sandboxed child never imports it.
"PYTHONNOUSERSITE": "1",
# git runs repository hooks (.git/hooks/pre-commit, post-checkout, ...) as executable
# files in an UNGUARDED child; a sandboxed snippet could plant one and trigger it via a
# benign-looking git commit / merge / checkout. Point core.hooksPath at a non-directory
# (via git's env-config mechanism) so NO repository hook runs, for every git subcommand,
# without having to block git itself. Neutralizing hooks is the sandbox-correct default.
"GIT_CONFIG_COUNT": "1",
"GIT_CONFIG_KEY_0": "core.hooksPath",
"GIT_CONFIG_VALUE_0": os.devnull,
}
if venv:
env["VIRTUAL_ENV"] = venv
@ -3613,12 +3732,34 @@ def _kw_present(node, name):
return any(kw.arg == name for kw in node.keywords)
def _numpy_allow_pickle_unsafe(node):
"""True when numpy.load may unpickle: allow_pickle is a constant truthy, a present-but-non-
constant value (flag=True), or hidden in a **kwargs splat we cannot prove absent/False.
allow_pickle absent (default False) or a constant False stays safe."""
for kw in node.keywords:
if kw.arg == "allow_pickle":
if isinstance(kw.value, ast.Constant):
return bool(kw.value.value)
return True # non-literal value cannot be proven False
if kw.arg is None:
# **{...} / **var splat: inspect a literal dict, else fail closed.
if isinstance(kw.value, ast.Dict):
for _k, _v in zip(kw.value.keys, kw.value.values):
if isinstance(_k, ast.Constant) and _k.value == "allow_pickle":
if not isinstance(_v, ast.Constant) or bool(_v.value):
return True
else:
return True # opaque **var could carry allow_pickle=True
return False
def _pickle_loader_is_unsafe(fq, node):
"""True when a torch.load / numpy.load / joblib.load call runs an UNVERIFIED pickle payload:
joblib.load always does; torch.load when weights_only is EXPLICITLY not-True (torch>=2.6
defaults it to True, so the bare torch.load(f) form relies on that safe default and stays
allowed); numpy.load only when allow_pickle is a constant True. So the safe forms
(torch.load(f), torch.load(f, weights_only=True), numpy.load(f)) return False."""
allowed); numpy.load when allow_pickle is truthy / non-literal / splatted (see
_numpy_allow_pickle_unsafe). So the safe forms (torch.load(f), torch.load(f,
weights_only=True), numpy.load(f), numpy.load(f, allow_pickle=False)) return False."""
if fq == "joblib.load":
return True
if fq == "torch.load":
@ -3627,7 +3768,7 @@ def _pickle_loader_is_unsafe(fq, node):
and _kw_constant_truthy(node, "weights_only") is not True
)
if fq == "numpy.load":
return _kw_constant_truthy(node, "allow_pickle") is True
return _numpy_allow_pickle_unsafe(node)
return False
@ -3641,11 +3782,15 @@ def _yaml_loader_class_name(value):
def _yaml_call_has_safe_loader(node):
"""True only when a yaml.load(...) call passes an explicit safe Loader= keyword.
"""True only when a yaml.load(...) call passes an explicit safe loader.
A missing Loader (older PyYAML defaults to the full, unsafe loader), an unknown/computed
loader, or a **kwargs splat all fail closed so the call is treated as an unsafe sink.
PyYAML's signature is load(stream, Loader), so the loader may be the SECOND POSITIONAL
argument (yaml.load(data, yaml.SafeLoader)) or the Loader= keyword. A missing loader (older
PyYAML defaults to the full, unsafe loader), an unknown/computed loader, or a **kwargs splat
all fail closed so the call is treated as an unsafe sink.
"""
if len(node.args) >= 2:
return _yaml_loader_class_name(node.args[1]) in _YAML_SAFE_LOADERS
for kw in node.keywords:
if kw.arg == "Loader":
return _yaml_loader_class_name(kw.value) in _YAML_SAFE_LOADERS
@ -4915,12 +5060,14 @@ def _scan_command_string_for_reads(
# overridable per-command by env -C DIR. Resets to the ambient cwd at each separator.
_chdir = cwd
_pending_chdir = False
_pending_argfile = False
for _pi, _pt in enumerate(ptoks):
if _pt in _READ_SCAN_SEPARATORS:
_at_cmd = True
_cur_reader = False
_wrapper = None
_skip_operand = False
_pending_argfile = False
_chdir = cwd
_pending_chdir = False
continue
@ -4936,6 +5083,10 @@ def _scan_command_string_for_reads(
if _pending_chdir: # ...but env -C DIR's operand is the child cwd
_chdir = _join_chdir(_chdir, _pt)
_pending_chdir = False
elif _pending_argfile: # ...and xargs -a FILE reads FILE
_pending_argfile = False
if _risky_read_target(_pt):
return f"xargs reads arguments from a sensitive path {_pt!r}"
_skip_operand = False
continue
if _ASSIGNMENT_RE.match(_pt):
@ -4952,6 +5103,16 @@ def _scan_command_string_for_reads(
_skip_operand = True
elif _wrapper == "env" and _pt.startswith("--chdir="):
_chdir = _join_chdir(_chdir, _pt.split("=", 1)[1])
# xargs -a FILE / --arg-file[=]FILE reads its argument list FROM that file, so a
# sensitive / expanded target is a host-file read even though xargs is a wrapper.
elif _wrapper == "xargs" and _pt in ("-a", "--arg-file"):
_pending_argfile = True
_skip_operand = True
elif _wrapper == "xargs" and _pt.startswith("--arg-file="):
if _risky_read_target(_pt.split("=", 1)[1]):
return (
f"xargs reads arguments from a sensitive path {_pt.split('=', 1)[1]!r}"
)
elif _wrapper and _wrapper_flag_takes_operand(_wrapper, _pt):
_skip_operand = True
continue # wrapper flag; still before the command word

View file

@ -297,11 +297,18 @@ class TestSandboxEnvIsolation:
"PYTHONNOUSERSITE",
"VIRTUAL_ENV",
"SystemRoot",
# git hooks neutralized via git's env-config mechanism (no repository hook runs).
"GIT_CONFIG_COUNT",
"GIT_CONFIG_KEY_0",
"GIT_CONFIG_VALUE_0",
}
extras = set(env.keys()) - allowed
assert not extras, f"sandbox env added unexpected keys: {extras}"
# User site-packages must be disabled so a planted ~/.local usercustomize.py cannot run.
assert env["PYTHONNOUSERSITE"] == "1"
# git repository hooks must be neutralized (core.hooksPath -> a non-directory).
assert env["GIT_CONFIG_KEY_0"] == "core.hooksPath"
assert env["GIT_CONFIG_VALUE_0"] == os.devnull
def test_home_points_at_sandbox_workdir(self, tmp_path):
from core.inference.tools import _build_safe_env
@ -3781,3 +3788,112 @@ class TestRound35Bypasses:
)
def test_round35_benign_allowed(self, code):
_ok(code)
class TestRound36Bypasses:
"""Thirty-sixth-round Codex findings: a $VAR PATH component resolving to cwd, hash -p binding
a command to a local exec, find -fls, git --output escaping, env -C changing git's cwd, xargs
--arg-file reads, the watch wrapper, non-literal/splatted numpy allow_pickle, and the yaml
positional-loader false positive. Plus git repository hooks neutralized in the sandbox env."""
@pytest.mark.parametrize(
"code",
[
# A $VAR PATH component bound to a relative/cwd value (or a relative ${VAR:-.} default)
# lets a bare command resolve to a workdir executable.
"import os\nos.system('P=.; PATH=$P evil')",
"import os\nos.system('PATH=${P-.} evil')",
"import os\nos.system('PATH=${P:-.} evil')",
],
)
def test_var_expanded_path_cwd_blocked(self, code):
assert _check_code_safety(code) is not None, code
@pytest.mark.parametrize(
"code",
[
# hash -p PATH NAME binds NAME to a local executable that then runs unguarded.
"import os\nos.system('hash -p ./evil ls; ls')",
"import os\nos.system('hash -p ./tools/evil grep; grep x f')",
],
)
def test_hash_p_local_exec_blocked(self, code):
assert _check_code_safety(code) is not None, code
@pytest.mark.parametrize(
"code",
[
# find -fls writes its listing to a file; git --output / env -C git write outside.
"import os\nos.system('find . -maxdepth 0 -fls /tmp/escape')",
"import os\nos.system('git archive --output=/tmp/out HEAD')",
"import subprocess\nsubprocess.run(['git', 'archive', '-o', '/tmp/out', 'HEAD'])",
"import os\nos.system('env -C /tmp git init .')",
"import os\nos.system('env -C /tmp git init')",
],
)
def test_find_git_write_outside_blocked(self, code):
assert _check_code_safety(code) is not None, code
@pytest.mark.parametrize(
"code",
[
# xargs -a / --arg-file reads its argument list from a sensitive host file.
"import os\nos.system('xargs -a /etc/shadow echo')",
"import os\nos.system('xargs --arg-file=/etc/shadow echo')",
],
)
def test_xargs_arg_file_read_blocked(self, code):
assert _check_code_safety(code) is not None, code
@pytest.mark.parametrize(
"code",
[
# watch runs its command argument (via sh -c or exec -x) in an unguarded child.
"import os\nos.system('watch -x touch /tmp/x')",
"import os\nos.system('watch -n 2 rm -rf /tmp/x')",
"import os\nos.system('watch touch /tmp/x')",
],
)
def test_watch_wrapper_blocked(self, code):
assert _check_code_safety(code) is not None, code
@pytest.mark.parametrize(
"code",
[
# numpy.load unpickles when allow_pickle is truthy / non-literal / splatted.
"import numpy as np\nflag = True\nnp.load('x.npy', allow_pickle=flag)",
"import numpy as np\nnp.load('x.npy', **{'allow_pickle': True})",
"import numpy as np\nnp.load('x.npy', **kw)",
],
)
def test_numpy_allow_pickle_dynamic_blocked(self, code):
assert _check_code_safety(code) is not None, code
def test_git_hooks_neutralized_in_sandbox_env(self, tmp_path):
from core.inference.tools import _build_safe_env
env = _build_safe_env(str(tmp_path))
# core.hooksPath -> a non-directory disables every repository hook for sandboxed git.
assert env.get("GIT_CONFIG_COUNT") == "1"
assert env.get("GIT_CONFIG_KEY_0") == "core.hooksPath"
assert env.get("GIT_CONFIG_VALUE_0") == os.devnull
@pytest.mark.parametrize(
"code",
[
# yaml.load with a POSITIONAL safe loader (yaml.load(data, yaml.SafeLoader)) is safe
# and must NOT be blocked; other benign forms stay allowed too.
"import yaml\nyaml.load(data, yaml.SafeLoader)",
"import yaml\nyaml.load(data, Loader=yaml.SafeLoader)",
"import yaml\nyaml.safe_load(data)",
"import os\nos.system('PATH=$CONDA_PREFIX/bin:$PATH true')",
"import os\nos.system('hash -r')",
"import os\nos.system('find . -name \"*.py\"')",
"import os\nos.system('git archive HEAD')",
"import os\nos.system('env -C sub ls')",
"import os\nos.system('env -C /app cat readme.md')",
"import numpy as np\nnp.load('a.npy', allow_pickle=False)",
],
)
def test_round36_benign_allowed(self, code):
_ok(code)