core/tools: block a denylisted program behind env wrappers and run launchers

This commit is contained in:
2026-10-06 19:42:06 +08:00
parent 6453a8ac89
commit 9501efb3d7
2 changed files with 85 additions and 17 deletions
+23 -17
View File
@@ -5,6 +5,8 @@ from enum import IntEnum
from pathlib import Path
from typing import TYPE_CHECKING, cast
from plyngent.tools.command_scan import unwrap_command
if TYPE_CHECKING:
from collections.abc import Callable, Mapping, Sequence
@@ -404,7 +406,12 @@ def argv_shape_error(argv: object) -> str | None:
def check_command_allowed(argv: list[str]) -> None:
"""Raise if argv is empty or the executable basename is denylisted.
"""Raise if argv is empty or a basename in the command chain is denylisted.
The chain is the program that really runs plus every wrapper it is reached
through (``env FOO=1 rm``, ``pdm run rm``, ``sudo -u x rm``), so a denylisted
program cannot hide behind one. Basenames are compared lower-cased, so
``RM`` and ``rm.exe`` count as ``rm``.
Denylisted basenames are not hard-rejected when a policy confirm hook is
installed: the human is asked (with a timeout; default deny). Session grants
@@ -418,25 +425,24 @@ def check_command_allowed(argv: list[str]) -> None:
msg = "command argv must not be empty"
raise WorkspaceError(msg)
policy = active_workspace_policy()
binary = Path(argv[0]).name
if binary not in policy.command_denylist:
return
if binary in policy.policy_allowed_commands:
return
hook = policy.policy_confirm_hook
if hook is not None:
scan = unwrap_command(argv)
for binary in (*scan.wrappers, scan.base):
if binary not in policy.command_denylist or binary in policy.policy_allowed_commands:
continue
hook = policy.policy_confirm_hook
if hook is None:
msg = f"command denied by policy (basename {binary!r} is blocked)"
raise WorkspaceError(msg)
timeout = policy.policy_confirm_timeout_seconds
try:
allowed = bool(hook(binary, list(argv), timeout))
except Exception as exc:
msg = f"command denied by policy (basename {binary!r}; confirm failed: {exc})"
raise WorkspaceError(msg) from exc
if allowed:
policy.policy_allowed_commands.add(binary)
return
msg = (
f"command denied by policy (basename {binary!r} is blocked; user declined or timed out after {timeout:g}s)"
)
raise WorkspaceError(msg)
msg = f"command denied by policy (basename {binary!r} is blocked)"
raise WorkspaceError(msg)
if not allowed:
msg = (
f"command denied by policy (basename {binary!r} is blocked; "
f"user declined or timed out after {timeout:g}s)"
)
raise WorkspaceError(msg)
policy.policy_allowed_commands.add(binary)
+62
View File
@@ -215,6 +215,68 @@ def test_command_denylist_policy_confirm_timeout_value(workspace: object) -> Non
clear_policy_allowed_commands()
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
set_policy_confirm_hook(None)
clear_policy_allowed_commands()
try:
with pytest.raises(WorkspaceError, match="basename 'rm' is blocked"):
check_command_allowed(["env", "FOO=1", "rm", "-rf", "/"])
with pytest.raises(WorkspaceError, match="basename 'rm' is blocked"):
check_command_allowed(["pdm", "run", "rm", "-rf", "/"])
with pytest.raises(WorkspaceError, match="basename 'rm' is blocked"):
check_command_allowed(["timeout", "5", "rm", "-rf", "/"])
# The outermost denylisted program is the one named (``sudo`` here).
with pytest.raises(WorkspaceError, match="basename 'sudo' is blocked"):
check_command_allowed(["sudo", "-u", "root", "rm", "-rf", "/"])
# Wrapped programs that are not denylisted stay allowed.
check_command_allowed(["pdm", "run", "pytest", "-x"])
check_command_allowed(["env", "FOO=1", "echo", "ok"])
finally:
clear_policy_allowed_commands()
def test_command_denylist_matches_lowercased_basename(workspace: object) -> None:
del workspace
from plyngent.tools.workspace import clear_policy_allowed_commands, set_policy_confirm_hook
set_policy_confirm_hook(None)
clear_policy_allowed_commands()
try:
with pytest.raises(WorkspaceError, match="basename 'rm' is blocked"):
check_command_allowed(["RM", "-rf", "/"])
with pytest.raises(WorkspaceError, match="basename 'rm' is blocked"):
check_command_allowed(["C:\\Tools\\rm.exe", "-rf", "/"])
finally:
clear_policy_allowed_commands()
def test_command_denylist_prompts_for_the_wrapped_program(workspace: object) -> None:
del workspace
from plyngent.tools.workspace import clear_policy_allowed_commands, set_policy_confirm_hook
asked: list[str] = []
def hook(basename: str, argv: object, timeout: float) -> bool:
del argv, timeout
asked.append(basename)
return True
set_policy_confirm_hook(hook)
clear_policy_allowed_commands()
try:
check_command_allowed(["pdm", "run", "rm", "-rf", "build"])
assert asked == ["rm"]
# Session grant: the same wrapped program does not re-prompt.
check_command_allowed(["pdm", "run", "rm", "-rf", "dist"])
assert asked == ["rm"]
finally:
set_policy_confirm_hook(None)
clear_policy_allowed_commands()
def test_root_required() -> None:
from plyngent.tools.context import InstanceState, bind_instance