mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-20 12:13:38 +08:00
fix: harden destructive command matching
This commit is contained in:
@@ -102,18 +102,7 @@ agent_loop(history)
|
||||
**PreToolUse / PostToolUse**、ツール実行の前後のフック。s03 の権限チェックロジックは PreToolUse フックに包まれ、さらにログフックと大出力リマインダーが追加される:
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
DESTRUCTIVE_COMMAND_WORD = re.compile(
|
||||
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
|
||||
)
|
||||
|
||||
|
||||
def contains_destructive_command(command: str) -> bool:
|
||||
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
|
||||
|
||||
|
||||
# PreToolUse: 権限チェック(s03 のロジック、ループからフックに移動)
|
||||
# PreToolUse: 権限チェック(s03 から引き継いだ matcher を含む)
|
||||
def permission_hook(block):
|
||||
if block.name == "bash":
|
||||
command = block.input.get("command", "")
|
||||
@@ -144,8 +133,6 @@ register_hook("PreToolUse", log_hook)
|
||||
register_hook("PostToolUse", large_output_hook)
|
||||
```
|
||||
|
||||
継承された shell rule は大文字小文字を区別せず、command の先頭または shell separator の直後にある完全な `rm`/`del` command word だけを検出する。`model`、`delimiter`、`echo del test.txt` は危険な command として扱わない。
|
||||
|
||||
**Stop** はループが終了する直前に発火する。以下の hook は終了時の統計を出力する:
|
||||
|
||||
```python
|
||||
|
||||
@@ -102,18 +102,7 @@ agent_loop(history)
|
||||
**PreToolUse / PostToolUse**, hooks before and after tool execution. s03's permission check logic is now wrapped as a PreToolUse hook, plus a logging hook and a large-output reminder:
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
DESTRUCTIVE_COMMAND_WORD = re.compile(
|
||||
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
|
||||
)
|
||||
|
||||
|
||||
def contains_destructive_command(command: str) -> bool:
|
||||
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
|
||||
|
||||
|
||||
# PreToolUse: permission check (s03 logic, moved from loop to hook)
|
||||
# PreToolUse: permission check (including the matcher inherited from s03)
|
||||
def permission_hook(block):
|
||||
if block.name == "bash":
|
||||
command = block.input.get("command", "")
|
||||
@@ -144,8 +133,6 @@ register_hook("PreToolUse", log_hook)
|
||||
register_hook("PostToolUse", large_output_hook)
|
||||
```
|
||||
|
||||
The inherited shell rule is case-insensitive and matches a complete `rm` or `del` command word only at the start of a command or after a shell separator. It does not match `model`, `delimiter`, or `echo del test.txt`.
|
||||
|
||||
**Stop** triggers when the loop is about to exit. The following hook prints a cleanup summary:
|
||||
|
||||
```python
|
||||
|
||||
@@ -102,18 +102,7 @@ agent_loop(history)
|
||||
**PreToolUse / PostToolUse**,工具执行前后的 hook。s03 的权限检查逻辑现在包装成 PreToolUse hook,再加一个日志 hook 和一个大输出提醒:
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
DESTRUCTIVE_COMMAND_WORD = re.compile(
|
||||
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
|
||||
)
|
||||
|
||||
|
||||
def contains_destructive_command(command: str) -> bool:
|
||||
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
|
||||
|
||||
|
||||
# PreToolUse: 权限检查(s03 的逻辑,从循环移到 hook)
|
||||
# PreToolUse: 权限检查(包含从 s03 沿用的 matcher)
|
||||
def permission_hook(block):
|
||||
if block.name == "bash":
|
||||
command = block.input.get("command", "")
|
||||
@@ -144,8 +133,6 @@ register_hook("PreToolUse", log_hook)
|
||||
register_hook("PostToolUse", large_output_hook)
|
||||
```
|
||||
|
||||
沿用的 shell 规则不区分大小写,只在命令开头或 shell 分隔符之后识别完整的 `rm`/`del` 命令词。`model`、`delimiter` 和 `echo del test.txt` 不会被识别为危险命令。
|
||||
|
||||
**Stop** 在循环即将退出时触发。以下 hook 打印收尾统计:
|
||||
|
||||
```python
|
||||
|
||||
@@ -22,6 +22,7 @@ Hooks run callbacks at fixed points in the agent loop:
|
||||
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
@@ -140,14 +141,191 @@ def trigger_hooks(event: str, *args):
|
||||
|
||||
# s03 permission check logic, now wrapped as a hook
|
||||
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
|
||||
DESTRUCTIVE_COMMAND_WORD = re.compile(
|
||||
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
|
||||
SHELL_SEPARATORS = ";&|\n"
|
||||
DESTRUCTIVE_COMMANDS = {"rm", "del"}
|
||||
SHELL_WRAPPERS = {"sh", "bash", "zsh", "dash", "cmd", "cmd.exe"}
|
||||
COMMAND_PREFIXES = {"command", "call"}
|
||||
CONTROL_PREFIXES = {"then", "do", "else", "!", "{"}
|
||||
COMPARE_OPERATORS = {"equ", "neq", "lss", "leq", "gtr", "geq"}
|
||||
MAX_COMMAND_NESTING = 16
|
||||
DESTRUCTIVE_SUBCOMMAND = re.compile(
|
||||
r"(?i)(?:\$\(|[<>]\(|\x60)\s*(?:rm|del)"
|
||||
r"(?=\s|$|[;&|()])"
|
||||
)
|
||||
DESTRUCTIVE = ["> /etc/", "chmod 777"]
|
||||
|
||||
|
||||
def contains_destructive_command(command: str) -> bool:
|
||||
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
|
||||
def shell_tokens(command: str) -> list[str]:
|
||||
lexer = shlex.shlex(
|
||||
command, posix=False, punctuation_chars=SHELL_SEPARATORS
|
||||
)
|
||||
lexer.whitespace = " \t\r"
|
||||
lexer.whitespace_split = True
|
||||
lexer.commenters = ""
|
||||
return list(lexer)
|
||||
|
||||
|
||||
def shell_syntax_outside_single_quotes(command: str) -> str:
|
||||
visible = []
|
||||
single_quoted = double_quoted = escaped = False
|
||||
for char in command:
|
||||
if escaped:
|
||||
visible.append(" ")
|
||||
escaped = False
|
||||
elif char == "\\" and not single_quoted:
|
||||
visible.append(" ")
|
||||
escaped = True
|
||||
elif char == '"' and not single_quoted:
|
||||
double_quoted = not double_quoted
|
||||
visible.append(char)
|
||||
elif char == "'" and not double_quoted:
|
||||
single_quoted = not single_quoted
|
||||
visible.append(" ")
|
||||
else:
|
||||
visible.append(" " if single_quoted else char)
|
||||
return "".join(visible)
|
||||
|
||||
|
||||
def unquote_shell_token(token: str) -> str:
|
||||
if len(token) >= 2 and token[0] in "'\"" and token[-1] == token[0]:
|
||||
return token[1:-1]
|
||||
return token
|
||||
|
||||
|
||||
def command_name(token: str) -> str:
|
||||
value = unquote_shell_token(token).lstrip("@").strip("()").casefold()
|
||||
if value.startswith("del/"):
|
||||
return "del"
|
||||
return value.replace("\\", "/").rsplit("/", 1)[-1]
|
||||
|
||||
|
||||
def is_shell_separator(token: str) -> bool:
|
||||
return bool(token) and all(char in SHELL_SEPARATORS for char in token)
|
||||
|
||||
|
||||
def is_shell_assignment(token: str) -> bool:
|
||||
name, separator, _ = unquote_shell_token(token).partition("=")
|
||||
return bool(
|
||||
separator
|
||||
and name
|
||||
and not name[0].isdigit()
|
||||
and name.replace("_", "a").isalnum()
|
||||
)
|
||||
|
||||
|
||||
def segment_has_destructive_command(
|
||||
tokens: list[str], depth: int = 0
|
||||
) -> bool:
|
||||
if depth >= MAX_COMMAND_NESTING:
|
||||
return True
|
||||
|
||||
index = 0
|
||||
while index < len(tokens) and is_shell_assignment(tokens[index]):
|
||||
index += 1
|
||||
if index >= len(tokens):
|
||||
return False
|
||||
|
||||
name = command_name(tokens[index])
|
||||
if name in DESTRUCTIVE_COMMANDS:
|
||||
return True
|
||||
if name in CONTROL_PREFIXES:
|
||||
return segment_has_destructive_command(tokens[index + 1:], depth + 1)
|
||||
if name == "env":
|
||||
index += 1
|
||||
while index < len(tokens) and (
|
||||
unquote_shell_token(tokens[index]).startswith("-")
|
||||
or is_shell_assignment(tokens[index])
|
||||
):
|
||||
index += 1
|
||||
return segment_has_destructive_command(tokens[index:], depth + 1)
|
||||
if name in COMMAND_PREFIXES:
|
||||
index += 1
|
||||
options = []
|
||||
while (
|
||||
index < len(tokens)
|
||||
and unquote_shell_token(tokens[index]).startswith("-")
|
||||
):
|
||||
options.append(unquote_shell_token(tokens[index]))
|
||||
index += 1
|
||||
if name == "command" and any(
|
||||
"v" in option.lstrip("-").casefold() for option in options
|
||||
):
|
||||
return False
|
||||
return segment_has_destructive_command(tokens[index:], depth + 1)
|
||||
if name in SHELL_WRAPPERS:
|
||||
for flag_index in range(index + 1, len(tokens)):
|
||||
flag = unquote_shell_token(tokens[flag_index]).casefold()
|
||||
is_command_flag = (
|
||||
flag in {"/c", "/k"}
|
||||
if name.startswith("cmd")
|
||||
else flag.startswith("-")
|
||||
and not flag.startswith("--")
|
||||
and "c" in flag[1:]
|
||||
)
|
||||
if is_command_flag:
|
||||
nested = " ".join(
|
||||
unquote_shell_token(token)
|
||||
for token in tokens[flag_index + 1:]
|
||||
)
|
||||
return contains_destructive_command(nested, depth + 1)
|
||||
return False
|
||||
if name == "if":
|
||||
index += 1
|
||||
while (
|
||||
index < len(tokens)
|
||||
and command_name(tokens[index]) in {"/i", "not"}
|
||||
):
|
||||
index += 1
|
||||
if index >= len(tokens):
|
||||
return False
|
||||
condition = command_name(tokens[index])
|
||||
if condition in {"exist", "defined", "errorlevel", "cmdextversion"}:
|
||||
return segment_has_destructive_command(
|
||||
tokens[index + 2:], depth + 1
|
||||
)
|
||||
if "==" in unquote_shell_token(tokens[index]):
|
||||
return segment_has_destructive_command(
|
||||
tokens[index + 1:], depth + 1
|
||||
)
|
||||
if (
|
||||
index + 2 < len(tokens)
|
||||
and command_name(tokens[index + 1]) in COMPARE_OPERATORS
|
||||
):
|
||||
return segment_has_destructive_command(
|
||||
tokens[index + 3:], depth + 1
|
||||
)
|
||||
return False
|
||||
if name == "for":
|
||||
for do_index, token in enumerate(tokens[index + 1:], index + 1):
|
||||
if command_name(token) == "do":
|
||||
return segment_has_destructive_command(
|
||||
tokens[do_index + 1:], depth + 1
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def contains_destructive_command(command: str, depth: int = 0) -> bool:
|
||||
if depth >= MAX_COMMAND_NESTING:
|
||||
return True
|
||||
|
||||
try:
|
||||
tokens = shell_tokens(command)
|
||||
except ValueError:
|
||||
return True
|
||||
if DESTRUCTIVE_SUBCOMMAND.search(
|
||||
shell_syntax_outside_single_quotes(command)
|
||||
):
|
||||
return True
|
||||
|
||||
segment = []
|
||||
for token in tokens:
|
||||
if is_shell_separator(token):
|
||||
if segment_has_destructive_command(segment, depth):
|
||||
return True
|
||||
segment = []
|
||||
else:
|
||||
segment.append(token)
|
||||
return segment_has_destructive_command(segment, depth)
|
||||
|
||||
|
||||
def permission_hook(block):
|
||||
|
||||
Reference in New Issue
Block a user