core/tools: only value-taking options may swallow the program

The wrapper option table listed flags as if they consumed the next token, so
the program behind them was skipped: `exec -c git -c init.defaultBranch=main
init` resolved to `init` and the policy denied it as a denylisted basename,
while `exec -c rm -rf /`, `command -p rm -rf /` and `command -v rm` hid `rm`
from the denylist completely and `exec -c python -c …` lost the interpreter
review. Drop the entries that take no value — `exec -c`/`-l`, the whole
`command` entry, `pkexec --disable-internal-agent`, `su -p`, and `xargs -e`
(its argument is optional, and the jammed form needs no entry) — so the
resolved program is the one that really runs.
This commit is contained in:
2026-10-06 21:50:21 +08:00
parent e05dc5127a
commit 2b3b2073a0
4 changed files with 68 additions and 6 deletions
+27
View File
@@ -65,6 +65,33 @@ def test_unwrap_stops_when_no_program_follows() -> None:
assert scan.base == argv[0], argv
def test_unwrap_value_free_flags_do_not_swallow_the_program() -> None:
"""Only options that consume the next token may be listed as value-taking.
``exec -c`` (empty environment) and ``exec -l`` (login), ``command
-p|-v|-V``, ``pkexec --disable-internal-agent`` and ``su -p`` are flags: a
table entry for them ate the program behind them, so ``exec -c rm -rf /``
hid ``rm`` from the denylist while ``exec -c git -c … init`` resolved to
``init``.
"""
cases = [
(["exec", "-c", "rm", "-rf", "/"], "rm"),
(["exec", "-c", "git", "-c", "init.defaultBranch=main", "init"], "git"),
(["exec", "-l", "python", "script.py"], "python"),
(["command", "-v", "rm"], "rm"),
(["command", "-p", "rm", "-rf", "/"], "rm"),
(["pkexec", "--disable-internal-agent", "rm", "-rf", "/"], "rm"),
(["su", "-p", "python", "script.py"], "python"),
# Options that do take a value still consume it.
(["exec", "-a", "name", "rm", "-rf", "/"], "rm"),
(["xargs", "-e", "rm", "-rf", "/"], "rm"),
]
for argv, base in cases:
scan = unwrap_command(argv)
assert scan.base == base, argv
assert scan.command[0] == base, argv
def test_unwrap_empty_argv() -> None:
scan = unwrap_command([])
assert scan.base == ""
+8
View File
@@ -136,6 +136,14 @@ def test_wrappers_reach_the_interpreter() -> None:
assert "$(command)" in reason
def test_value_free_flags_do_not_hide_the_interpreter() -> None:
"""``exec -c`` takes no value, so the interpreter behind it is still reviewed."""
reason = classify_danger("run_argv", {"argv": ["exec", "-c", "python", "-c", "print(1)"]})
assert reason is not None
assert "interpreter 'python'" in reason
assert "print(1)" in reason
def test_only_the_interpreter_dash_c_becomes_a_placeholder() -> None:
reason = classify_danger("run_argv", {"argv": ["ionice", "-c", "2", "python", "-c", "print(1)"]})
assert reason is not None
+24
View File
@@ -215,6 +215,30 @@ def test_command_denylist_policy_confirm_timeout_value(workspace: object) -> Non
clear_policy_allowed_commands()
def test_command_denylist_sees_the_program_behind_value_free_flags(workspace: object) -> None:
"""`exec -c` / `command -p` take no value, so the program stays visible."""
del workspace
from plyngent.tools.workspace import clear_policy_allowed_commands, set_policy_confirm_hook
set_policy_confirm_hook(None)
clear_policy_allowed_commands()
set_command_denylist(None)
try:
for argv in (
["exec", "-c", "rm", "-rf", "/"],
["command", "-p", "rm", "-rf", "/"],
["command", "-v", "rm"],
):
with pytest.raises(WorkspaceError, match="basename 'rm' is blocked"):
check_command_allowed(list(argv))
# ``init`` here is a git subcommand, not the denylisted program: the
# command resolves to ``git`` and stays allowed.
check_command_allowed(["exec", "-c", "git", "-c", "init.defaultBranch=main", "init"])
finally:
clear_policy_allowed_commands()
set_command_denylist(None)
def test_command_denylist_reaches_through_wrappers(workspace: object) -> None:
del workspace
from plyngent.tools.workspace import clear_policy_allowed_commands, set_policy_confirm_hook