fix: treat single ampersand as shell segment

Maintainer edit: single '&' backgrounds the preceding command and starts another top-level shell segment, so allowPatterns must split it the same way as ';', '|', '&&', and '||'. Keep fd redirections such as 2>&1 and &> intact.
This commit is contained in:
chengyongru
2026-07-21 13:50:24 +08:00
committed by Xubin Ren
parent f4a7079e65
commit 12c52c11d3
2 changed files with 22 additions and 0 deletions
+5
View File
@@ -879,6 +879,11 @@ class ExecTool(Tool):
if paren_depth == 0: if paren_depth == 0:
if command.startswith(("&&", "||"), i): if command.startswith(("&&", "||"), i):
operator_len = 2 operator_len = 2
elif ch == "&":
prev_ch = command[i - 1] if i > 0 else ""
next_ch = command[i + 1] if i + 1 < len(command) else ""
if prev_ch not in {"<", ">"} and next_ch != ">":
operator_len = 1
elif ch in {";", "|"}: elif ch in {";", "|"}:
operator_len = 1 operator_len = 1
+17
View File
@@ -67,6 +67,23 @@ def test_guard_allow_patterns_block_non_matching_chained_segment():
assert "allowlist" in result.lower() assert "allowlist" in result.lower()
def test_guard_allow_patterns_block_single_ampersand_chained_segment():
"""A backgrounded command is also a top-level shell segment."""
tool = ExecTool(allow_patterns=[r"echo\s+allowlisted.*"])
result = tool._guard_command("echo allowlisted & touch /tmp/evil", "/tmp")
assert result is not None
assert "allowlist" in result.lower()
def test_guard_allow_patterns_keep_fd_redirection_ampersand():
tool = ExecTool(allow_patterns=[r"echo\s+allowlisted\s+2>&1"])
result = tool._guard_command("echo allowlisted 2>&1", "/tmp")
assert result is None
def test_deny_patterns_search_original_command_with_quoted_hash(): def test_deny_patterns_search_original_command_with_quoted_hash():
"""Deny checks must still inspect text after a quoted hash.""" """Deny checks must still inspect text after a quoted hash."""
tool = ExecTool(deny_patterns=[r"\brm\s+-rf\s+/"]) tool = ExecTool(deny_patterns=[r"\brm\s+-rf\s+/"])