Harden sandbox: sqlite3 stdin SQL, sed -f scripts, git EDITOR/VISUAL + object-dir + exec-env command + marks-file, find {} exec, openssl -out=; scope assignment prefixes
Close nine command-scanner gaps Codex found on the round-54 branch (8 P1 + 1 P2).
- sqlite3 stdin SQL: sqlite3 [OPTIONS] [FILENAME [SQL]] reads SQL from stdin when
no SQL argv is given, so printf '.shell touch /tmp/p' | sqlite3 :memory: ran an
unscanned dot-command in the unguarded child. Fail closed when sqlite3 has no
inline SQL and a stdin source (a pipe target or a < / heredoc redirect).
- sed -f script files: the sed mutating-script check only scanned -e / positional
scripts, so sed -n -f evil.sed loaded w / e commands from an uninspectable
workdir file. Fail closed on any -f / --file form (separated, glued, combined
short group).
- git EDITOR / VISUAL fallbacks: the git exec-env allowlist covered GIT_EDITOR but
not the standard EDITOR / VISUAL fallbacks git uses for commit/tag messages.
Treat EDITOR / VISUAL like the git exec-env vars when the command is git (or the
value is exported).
- git object-directory env vars: GIT_OBJECT_DIRECTORY / GIT_COMMON_DIR /
GIT_ALTERNATE_OBJECT_DIRECTORIES point git's object store outside the workdir
(GIT_OBJECT_DIRECTORY=/tmp git hash-object -w), but only GIT_DIR / GIT_WORK_TREE
/ GIT_INDEX_FILE were path-checked. Add them to the escaping-path env check.
- assignment prefixes are now command-position aware (P2 false positive): a
NAME=value shaped token is treated as an environment assignment only in the
command-prefix position of its segment (or as an export / declare arg), so echo
GIT_CONFIG_COUNT=0 and printf %s PATH=.:/bin are no longer rejected while a real
PATH=. prefix and export PATH=.:/bin still block.
- find {} exec placeholder: find substitutes {} with each matched path, so find .
-name evil -exec {} ';' executes a planted workdir file, but the reconstructed
exec-segment scan saw only the harmless-looking {}. Fail closed when the exec
command word is (or starts with) {}.
- openssl glued -out=FILE: the OpenSSL write check only handled a separated -out
FILE operand, so openssl rand -out=/tmp/p slipped. Parse the glued -out=... /
-writerand=... forms alongside the separated form.
- commands in git exec-env vars: the exec-env check only rejected a local
executable path, so GIT_EXTERNAL_DIFF='touch /tmp/p' git diff (a bare system
command that writes outside) passed. Run the value through the command scanner,
which flags the write / escaping command.
- git marks-file options: git fast-export --export-marks=/tmp/marks (and
fast-import --import-marks) write / read an escaping path outside the small
_GIT_PATH_VALUE_OPTIONS list. Add the marks-file options to the path check.
Regression coverage: TestRound55Bypasses in tests/test_sandbox_tools.py (sqlite3
stdin, sed -f, git EDITOR/VISUAL incl. export, GIT_OBJECT_DIRECTORY / GIT_COMMON_DIR,
find {} exec, openssl -out=, git exec-env command values, git marks-file options,
the assignment-prefix position-awareness matrix, and a round55 benign-allowed set:
inline-SQL sqlite3, sed -e / bare script, EDITOR=vim, relative GIT_OBJECT_DIRECTORY,
find -exec cat {}, openssl -out=key, GIT_PAGER=cat, relative export-marks, export
of a benign var).
This commit is contained in:
parent
8bb6ed501a
commit
5172f6893d
2 changed files with 268 additions and 26 deletions
|
|
@ -600,6 +600,11 @@ _GIT_PATH_VALUE_OPTIONS = frozenset(
|
|||
"--output",
|
||||
"-O",
|
||||
"--output-directory",
|
||||
# fast-export / fast-import marks files: git writes / reads the given path from its
|
||||
# unguarded child (git fast-export --export-marks=/tmp/marks HEAD).
|
||||
"--export-marks",
|
||||
"--import-marks",
|
||||
"--import-marks-if-exists",
|
||||
}
|
||||
)
|
||||
# git config keys whose value is a COMMAND git runs in an unguarded child (git -c KEY=CMD ... /
|
||||
|
|
@ -1459,6 +1464,16 @@ def _find_blocked_commands(command: str) -> set[str]:
|
|||
seg.append(tokens[j])
|
||||
j += 1
|
||||
if seg:
|
||||
# find substitutes `{}` with each matched path, so `-exec {} ;` EXECUTES the matched
|
||||
# file -- a prior step can plant an executable ./evil and `find . -name evil -exec {}
|
||||
# ';'` then runs that workdir shebang in an unguarded child. The reconstructed
|
||||
# segment scan sees only the harmless-looking `{}`, so fail closed when the exec
|
||||
# command word itself is (or starts with) the placeholder.
|
||||
_ecw = seg[0]
|
||||
if len(_ecw) >= 2 and _ecw[0] == _ecw[-1] and _ecw[0] in ("'", '"'):
|
||||
_ecw = _ecw[1:-1]
|
||||
if _ecw == "{}" or _ecw.startswith("{}"):
|
||||
blocked.add("find-exec-placeholder")
|
||||
blocked |= _find_blocked_commands(" ".join(seg))
|
||||
|
||||
# Regex catches blocked words at command boundaries shlex misses: inside
|
||||
|
|
@ -1723,19 +1738,57 @@ def _find_blocked_commands(command: str) -> set[str]:
|
|||
if _an in ("BASH_ENV", "ENV") and _av != "":
|
||||
blocked.add("shell-startup-env:" + _an)
|
||||
|
||||
# A NAME=value token is an ENVIRONMENT assignment only in the command-PREFIX position (before
|
||||
# the command word of its segment); after the command word it is an ARGUMENT the shell does not
|
||||
# export (echo GIT_CONFIG_COUNT=0, printf %s PATH=.:/bin). Map each leading assignment token to
|
||||
# its segment's command-word index (None if the segment is pure assignments).
|
||||
def _assignment_prefix_map():
|
||||
_cmd_sorted = sorted(_cmd_word_idx)
|
||||
_bounds = []
|
||||
_seg_start = 0
|
||||
for _j in range(len(tokens) + 1):
|
||||
if (
|
||||
_j == len(tokens)
|
||||
or tokens[_j] in _SHELL_SEPARATORS
|
||||
or tokens[_j] in _SHELL_KEYWORDS_AS_SEP
|
||||
):
|
||||
if _j > _seg_start:
|
||||
_bounds.append((_seg_start, _j))
|
||||
_seg_start = _j + 1
|
||||
_out = {}
|
||||
for _a, _b in _bounds:
|
||||
_cw = None
|
||||
for _w in _cmd_sorted:
|
||||
if _a <= _w < _b:
|
||||
_cw = _w
|
||||
break
|
||||
# `export NAME=value` / `declare -x` / `typeset` set an env var even though NAME=value
|
||||
# follows the command word, so their NAME=value ARGS are assignments too.
|
||||
_exporter = _cw is not None and _token_basename(tokens[_cw]) in (
|
||||
"export",
|
||||
"declare",
|
||||
"typeset",
|
||||
)
|
||||
for _j in range(_a, _b):
|
||||
if _ASSIGNMENT_RE.match(tokens[_j]) and (_cw is None or _j < _cw or _exporter):
|
||||
_out[_j] = _cw
|
||||
return _out
|
||||
|
||||
_assign_prefix = _assignment_prefix_map()
|
||||
|
||||
# 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.
|
||||
# variable (P=.; PATH=$P evil) can be resolved to its (unsafe) value. Only real prefix
|
||||
# assignments count (not a NAME=value printed as an argument).
|
||||
_local_assigns = {}
|
||||
for _et in tokens:
|
||||
if _ASSIGNMENT_RE.match(_et):
|
||||
_n, _, _v = _et.partition("=")
|
||||
_local_assigns[_n.rstrip("+")] = _v
|
||||
for _ei in _assign_prefix:
|
||||
_n, _, _v = tokens[_ei].partition("=")
|
||||
_local_assigns[_n.rstrip("+")] = _v
|
||||
|
||||
# Assignment prefixes that persist for the command's child: a non-empty BASH_ENV / ENV (sourced
|
||||
# by a later shell), a PATH with a cwd entry (a bare command resolves to a workdir shebang), and
|
||||
# git path / config environment variables -- GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE point
|
||||
# git's writes outside the workdir, and GIT_CONFIG_* override the sandbox's env-based hook
|
||||
# suppression. Handle both NAME=value and NAME+=value (append).
|
||||
# git path / config environment variables -- GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE /
|
||||
# GIT_OBJECT_DIRECTORY / GIT_COMMON_DIR point git's repo / objects outside the workdir, and
|
||||
# GIT_CONFIG_* override the sandbox's env-based hook suppression. Handle NAME=value / NAME+=value.
|
||||
_GIT_EXEC_ENV_VARS = frozenset(
|
||||
{
|
||||
"GIT_EXTERNAL_DIFF",
|
||||
|
|
@ -1748,12 +1801,26 @@ def _find_blocked_commands(command: str) -> set[str]:
|
|||
"GIT_PAGER",
|
||||
}
|
||||
)
|
||||
for _ei, _et in enumerate(tokens):
|
||||
if not _ASSIGNMENT_RE.match(_et):
|
||||
continue
|
||||
# git path-valued repository env vars whose escaping value writes outside the workdir from an
|
||||
# unguarded git child (GIT_OBJECT_DIRECTORY=/tmp git hash-object -w --stdin).
|
||||
_GIT_PATH_ENV_VARS = frozenset(
|
||||
{
|
||||
"GIT_DIR",
|
||||
"GIT_WORK_TREE",
|
||||
"GIT_INDEX_FILE",
|
||||
"GIT_OBJECT_DIRECTORY",
|
||||
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
||||
"GIT_COMMON_DIR",
|
||||
}
|
||||
)
|
||||
for _ei, _cwidx in _assign_prefix.items():
|
||||
_et = tokens[_ei]
|
||||
_an, _, _av = _et.partition("=")
|
||||
_append = _an.endswith("+")
|
||||
_an = _an.rstrip("+")
|
||||
_cmd_base = _token_basename(tokens[_cwidx]) if _cwidx is not None else None
|
||||
_cmd_is_git = _cmd_base == "git"
|
||||
_is_exporter = _cmd_base in ("export", "declare", "typeset")
|
||||
if _an in ("BASH_ENV", "ENV") and _av != "":
|
||||
blocked.add("shell-startup-env:" + _an)
|
||||
# PATH=. cmd / PATH+=:. cmd: a relative / cwd entry lets a bare command word resolve to a
|
||||
|
|
@ -1763,9 +1830,10 @@ def _find_blocked_commands(command: str) -> set[str]:
|
|||
_pval = ("$PATH" + _av) if _append else _av
|
||||
if _path_value_is_unsafe(_pval, _local_assigns):
|
||||
blocked.add("unsafe-path-assign")
|
||||
# GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE set git's repo / tree / index path directly, so
|
||||
# an escaping value writes outside the workdir (GIT_DIR=/tmp/x git init) with no --git-dir.
|
||||
elif _an in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE") and _arg_escapes_workdir(_av):
|
||||
# GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE / GIT_OBJECT_DIRECTORY / ... set git's repo /
|
||||
# tree / index / object-store path directly, so an escaping value writes outside the workdir
|
||||
# (GIT_DIR=/tmp/x git init, GIT_OBJECT_DIRECTORY=/tmp git hash-object -w) with no CLI flag.
|
||||
elif _an in _GIT_PATH_ENV_VARS and _arg_escapes_workdir(_av):
|
||||
blocked.add("git-write-outside")
|
||||
# GIT_CONFIG[_GLOBAL/_SYSTEM/_COUNT/_KEY_*/_VALUE_*] re-point git config or drop the
|
||||
# sandbox's env-based hook suppression (GIT_CONFIG_COUNT=0 git ...), re-enabling a planted
|
||||
|
|
@ -1773,19 +1841,22 @@ def _find_blocked_commands(command: str) -> set[str]:
|
|||
elif _an == "GIT_CONFIG" or _an.startswith("GIT_CONFIG_"):
|
||||
blocked.add("git-config-env-override")
|
||||
# git runs the program named by these env vars (GIT_EXTERNAL_DIFF / GIT_ASKPASS /
|
||||
# GIT_SSH[_COMMAND] / GIT_PROXY_COMMAND / GIT_EDITOR / GIT_PAGER); a value pointing at a
|
||||
# WORKDIR executable (GIT_EXTERNAL_DIFF=./evil git diff) runs a planted helper in an
|
||||
# unguarded git child. A bare command name (GIT_PAGER=cat) or a system tool stays allowed.
|
||||
elif _an in _GIT_EXEC_ENV_VARS:
|
||||
# GIT_SSH[_COMMAND] / GIT_PROXY_COMMAND / GIT_EDITOR / GIT_PAGER), and for a git child the
|
||||
# standard EDITOR / VISUAL fallbacks name the commit-message editor too. Block a value that
|
||||
# points at a WORKDIR executable (GIT_EXTERNAL_DIFF=./evil), a ~ path, OR whose command the
|
||||
# scanner flags (GIT_EXTERNAL_DIFF='touch /tmp/p' -> touch writes outside). A bare system
|
||||
# command (GIT_PAGER=cat, EDITOR=vim) stays allowed.
|
||||
elif _an in _GIT_EXEC_ENV_VARS or (
|
||||
_an in ("EDITOR", "VISUAL") and (_cmd_is_git or _is_exporter)
|
||||
):
|
||||
_gev = _av
|
||||
if len(_gev) >= 2 and _gev[0] == _gev[-1] and _gev[0] in ("'", '"'):
|
||||
_gev = _gev[1:-1]
|
||||
_gecmd = _gev.split()[0] if _gev.split() else ""
|
||||
# A workdir-reachable helper: a local / relative executable (./evil, sub/evil) or a ~
|
||||
# (HOME == workdir) path. An absolute system tool (/usr/bin/ssh) and a bare PATH name
|
||||
# (cat) stay allowed -- the attacker cannot plant a file outside the workdir.
|
||||
if _is_local_executable_path(_gecmd) or _gecmd.startswith("~"):
|
||||
blocked.add("git-exec-env")
|
||||
elif _gev and _find_blocked_commands(_gev):
|
||||
blocked.add("git-exec-env")
|
||||
|
||||
# git -c alias.X='!CMD' X / git config alias.X '!CMD': a git alias whose value starts with
|
||||
# `!` runs CMD through an unguarded shell, but the scanner sees only `git`. Flag the shell-
|
||||
|
|
@ -2014,12 +2085,19 @@ def _find_blocked_commands(command: str) -> set[str]:
|
|||
t = tokens[k]
|
||||
if t in _SHELL_SEPARATORS or t in _SHELL_KEYWORDS_AS_SEP:
|
||||
break
|
||||
_op = None
|
||||
if t in _OPENSSL_WRITE_FLAGS and k + 1 < len(tokens):
|
||||
_op = tokens[k + 1]
|
||||
if _git_operand_escapes(_op, _local_assigns) or (
|
||||
_ossl_cwd_escapes and _operand_relative_local(_op)
|
||||
):
|
||||
blocked.add("openssl-write-outside")
|
||||
_op = tokens[k + 1] # separated form: -out FILE
|
||||
else:
|
||||
# glued form: -out=FILE / -writerand=FILE (openssl accepts -out outfile and =).
|
||||
_oflag, _oeq, _oval = t.partition("=")
|
||||
if _oeq and _oflag in _OPENSSL_WRITE_FLAGS:
|
||||
_op = _oval
|
||||
if _op is not None and (
|
||||
_git_operand_escapes(_op, _local_assigns)
|
||||
or (_ossl_cwd_escapes and _operand_relative_local(_op))
|
||||
):
|
||||
blocked.add("openssl-write-outside")
|
||||
|
||||
# sqlite3 <DBFILE> creates / opens a database in an unguarded child (no realpath guard), and
|
||||
# its dot-commands (.output / .backup / .dump / .read ...) read + write arbitrary files. Flag
|
||||
|
|
@ -2033,12 +2111,28 @@ def _find_blocked_commands(command: str) -> set[str]:
|
|||
# even a RELATIVE DBFILE / dot-file / -init operand (sqlite3 db.sqlite ..., cwd=/tmp) land
|
||||
# outside; combine the escaping cwd with a relative operand below.
|
||||
_sqlite_cwd_escapes = _cwd_wrapper_escapes(tokens, i)
|
||||
# sqlite3 [OPTIONS] [FILENAME [SQL]] reads SQL from STDIN when no SQL argv is given, so a
|
||||
# dot-command fed via a pipe or `<` redirect (printf '.shell touch /tmp/p\n' | sqlite3
|
||||
# :memory:) runs unscanned in the unguarded child. Detect a stdin source (this command is a
|
||||
# pipe target, or has a `<` / heredoc input redirect) with no inline SQL and fail closed.
|
||||
_sqlite_pipe_target = False
|
||||
for _bk in range(i - 1, -1, -1):
|
||||
_bt = tokens[_bk]
|
||||
if _bt in _SHELL_SEPARATORS or _bt in _SHELL_KEYWORDS_AS_SEP:
|
||||
_sqlite_pipe_target = _bt == "|"
|
||||
break
|
||||
_sqlite_stdin_redirect = False
|
||||
_seen_sql = False
|
||||
_seen_db = False
|
||||
_sk = i + 1
|
||||
while _sk < len(tokens):
|
||||
t = tokens[_sk]
|
||||
if t in _SHELL_SEPARATORS or t in _SHELL_KEYWORDS_AS_SEP:
|
||||
break
|
||||
if t in ("<", "<<", "<<<", "0<"):
|
||||
_sqlite_stdin_redirect = True
|
||||
_sk += 2 # skip the redirect target too
|
||||
continue
|
||||
# sqlite3 options that consume a SEPARATED operand; skip the value so it is not
|
||||
# mistaken for the DBFILE (only -init reads a file, checked via its own value here).
|
||||
if t in _SQLITE_OPERAND_OPTS:
|
||||
|
|
@ -2091,7 +2185,15 @@ def _find_blocked_commands(command: str) -> set[str]:
|
|||
or (_sqlite_cwd_escapes and _operand_relative_local(_dbn))
|
||||
):
|
||||
blocked.add("sqlite3-write-outside")
|
||||
else:
|
||||
# A bare operand after the DBFILE is inline SQL, so sqlite3 runs it and exits
|
||||
# WITHOUT reading stdin (already scanned for dot-commands above).
|
||||
_seen_sql = True
|
||||
_sk += 1
|
||||
# No inline SQL argv + a stdin source (pipe / redirect) means the dot-commands come from
|
||||
# unscanned stdin; fail closed (the .shell / .import / .output there are uninspectable).
|
||||
if not _seen_sql and (_sqlite_pipe_target or _sqlite_stdin_redirect):
|
||||
blocked.add("sqlite3-stdin-sql")
|
||||
|
||||
# watch runs its command via `sh -c '<operands joined>'` UNLESS -x/--exec is given (then it
|
||||
# execs argv directly, resolved by the wrapper handling above). So a quoted payload
|
||||
|
|
@ -2303,6 +2405,18 @@ def _find_blocked_commands(command: str) -> set[str]:
|
|||
# sed --expression='w /tmp/x'). Detect the write / execute commands and flags in the
|
||||
# script text; a plain s/word/x/ is not matched.
|
||||
if _base in ("sed", "gsed", "ssed"):
|
||||
# A -f / --file script file is loaded from disk and can carry the same
|
||||
# w / W / e / r mutating + exec commands as an inline script, but its
|
||||
# contents are not statically visible (a planted workdir evil.sed with
|
||||
# `1w /tmp/p`). Fail closed on any -f / --file form (separated, glued, or
|
||||
# combined short group like -nf).
|
||||
if (
|
||||
a in ("-f", "--file")
|
||||
or al.startswith("--file=")
|
||||
or (_short and "f" in al[1:])
|
||||
):
|
||||
blocked.add("sed-script-file:" + _base)
|
||||
break
|
||||
_sed_script = None
|
||||
if a in ("-e", "--expression") and k + 1 < len(tokens):
|
||||
_sed_script = tokens[k + 1] # -e SCRIPT (separated)
|
||||
|
|
|
|||
|
|
@ -5318,3 +5318,131 @@ class TestRound54Bypasses:
|
|||
)
|
||||
def test_round54_benign_allowed(self, code):
|
||||
_ok(code)
|
||||
|
||||
|
||||
def _sh(cmd):
|
||||
return "import os\nos.system(%r)" % cmd
|
||||
|
||||
|
||||
class TestRound55Bypasses:
|
||||
"""Fifty-fifth-round Codex findings: command-scanner coverage gaps -- sqlite3 stdin SQL, sed -f
|
||||
scripts, git EDITOR/VISUAL + object-dir env vars, find {} exec, openssl -out=, git exec-env
|
||||
command values, git marks-file options, plus the assignment-prefix false positive."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
# sqlite3 reads SQL from stdin (pipe / redirect) when no SQL argv is given.
|
||||
_sh("printf '.shell touch /tmp/p\\n' | sqlite3 :memory:"),
|
||||
_sh("sqlite3 :memory: < evil.sql"),
|
||||
],
|
||||
)
|
||||
def test_sqlite3_stdin_sql_blocked(self, code):
|
||||
assert _check_code_safety(code) is not None, code
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
# sed -f / --file loads an uninspectable script that can carry w / e commands.
|
||||
_sh("printf x | sed -n -f evil.sed"),
|
||||
_sh("sed --file=evil.sed data.txt"),
|
||||
_sh("sed -nf evil.sed data.txt"),
|
||||
],
|
||||
)
|
||||
def test_sed_script_file_blocked(self, code):
|
||||
assert _check_code_safety(code) is not None, code
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
# git EDITOR / VISUAL fallback runs the value as the commit-message editor.
|
||||
_sh("EDITOR='touch /tmp/p' git -c user.email=a@b -c user.name=c commit --allow-empty"),
|
||||
_sh("VISUAL='touch /tmp/p' git commit --allow-empty"),
|
||||
_sh("export EDITOR='touch /tmp/p'; git commit --allow-empty"),
|
||||
],
|
||||
)
|
||||
def test_git_editor_visual_blocked(self, code):
|
||||
assert _check_code_safety(code) is not None, code
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
# GIT_OBJECT_DIRECTORY / GIT_COMMON_DIR point git's object store outside the workdir.
|
||||
_sh("echo hi | GIT_OBJECT_DIRECTORY=/tmp git hash-object -w --stdin"),
|
||||
_sh("GIT_COMMON_DIR=/tmp git rev-parse"),
|
||||
],
|
||||
)
|
||||
def test_git_object_directory_env_blocked(self, code):
|
||||
assert _check_code_safety(code) is not None, code
|
||||
|
||||
def test_find_exec_placeholder_blocked(self):
|
||||
# find substitutes {} with the matched path, so -exec {} ; executes it.
|
||||
assert _check_code_safety(_sh("find . -name evil -exec {} ';'")) is not None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
# openssl glued -out=FILE / -writerand=FILE forms escape the workdir.
|
||||
_sh("openssl rand -out=/tmp/p 1"),
|
||||
_sh("openssl rand -writerand=/tmp/r 1"),
|
||||
],
|
||||
)
|
||||
def test_openssl_glued_out_blocked(self, code):
|
||||
assert _check_code_safety(code) is not None, code
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
# A bare command (with args) stored in a git exec-env var runs in an unguarded child.
|
||||
_sh("GIT_EXTERNAL_DIFF='touch /tmp/p' git diff"),
|
||||
_sh("GIT_SSH_COMMAND='rm -rf /tmp/x' git fetch"),
|
||||
],
|
||||
)
|
||||
def test_git_exec_env_command_value_blocked(self, code):
|
||||
assert _check_code_safety(code) is not None, code
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
# git fast-export / fast-import marks-file path options write / read an escaping path.
|
||||
_sh("git fast-export --export-marks=/tmp/marks HEAD"),
|
||||
_sh("git fast-import --import-marks=/tmp/m"),
|
||||
],
|
||||
)
|
||||
def test_git_marks_file_options_blocked(self, code):
|
||||
assert _check_code_safety(code) is not None, code
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
# A NAME=value shaped ARGUMENT to a printer is not an env assignment (P2 false positive),
|
||||
# but a real export / prefix assignment still blocks.
|
||||
(_sh("echo GIT_CONFIG_COUNT=0"), False),
|
||||
(_sh("printf %s PATH=.:/bin"), False),
|
||||
(_sh("PATH=. evilcmd"), True),
|
||||
(_sh("export PATH=.:/bin; evilcmd"), True),
|
||||
(_sh("export BASH_ENV=env.sh; bash -c 'echo ok'"), True),
|
||||
],
|
||||
)
|
||||
def test_assignment_prefix_position_aware(self, code):
|
||||
_c, _blocked = code
|
||||
assert (_check_code_safety(_c) is not None) is _blocked, code
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
# Benign forms across all round-55 checks stay allowed.
|
||||
_sh("sqlite3 local.db 'select 1;'"),
|
||||
_sh("sed -e 's/a/b/' data.txt"),
|
||||
_sh("sed 's/a/b/' data.txt"),
|
||||
_sh("EDITOR=vim git commit --allow-empty"),
|
||||
_sh("GIT_OBJECT_DIRECTORY=objs git hash-object -w --stdin"),
|
||||
_sh("find . -name '*.py' -exec cat {} ';'"),
|
||||
_sh("openssl rand -out=key 1"),
|
||||
_sh("GIT_PAGER=cat git log"),
|
||||
_sh("git fast-export --export-marks=marks HEAD"),
|
||||
_sh("export MYVAR=hello; echo hi"),
|
||||
],
|
||||
)
|
||||
def test_round55_benign_allowed(self, code):
|
||||
_ok(code)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue