fix(security): tokenize the execute_bash destructive-command screen (#128) - #195
Conversation
…S#128) The execute_bash guard matched destructive commands as substrings of the raw command string. Even after whitespace normalization it stayed bypassable: split short flags (rm -r -f), long flags (rm --recursive --force), and numeric permission variants (chmod 0777) all slip through, while benign commands that merely contain the text (touch rm-rf-notes.txt) are falsely blocked. Replace it with core.harness.command_guard.screen_command, which splits the command on shell operators (; && || | &) and tokenizes each segment with shlex, matching on argv (command name + flags) instead of substrings. This closes the flag-order / flag-spelling / numeric gaps and also catches a destructive stage hidden in a pipeline or sequence (echo hi && rm -rf /). This is cheap defense-in-depth, not the security boundary: the workspace sandbox remains the real enforcement, and any command the screen cannot parse is passed through to it unchanged. Add tests/test_command_guard.py covering the historical bypasses, the old false-positives, multi-segment commands, empty input, and unparseable input.
🔍 CI 失败诊断 (raymondginger2018-sudo)PR #195 的 4 个 CI job(test 3.12/3.13/3.14 + Windows lifecycle)全部失败,根因是同一处: 测试失败(3 个 parametrize 参数全挂)。 根因 1: 大小写敏感将 直接传给助手函数,但 检查 Microsoft Windows [°汾 10.0.26200.9278] F:\DEEPCODE>(大小写敏感)。大写输入 不被拦截,落到 sandbox 执行失败,消息变成 (不含 )。 修复: 后再分类 根因 2:error message 不含将 blocked 消息从 改为 ,但新增的 断言 。消息格式变更与测试断言不一致。 修复: → (消息变为 ) 修复 diff(2 行)diff --git a/core/harness/command_guard.py b/core/harness/command_guard.py
index a6b4bff1..17cc7e08 100644
--- a/core/harness/command_guard.py
+++ b/core/harness/command_guard.py
@@ -144,7 +144,7 @@ def screen_command(command: str) -> str | None:
continue
if not tokens:
continue
- reason = _classify(tokens[0], tokens[1:])
+ reason = _classify(tokens[0].lower(), tokens[1:])
if reason is not None:
return reason
diff --git a/tools/code_implementation_server.py b/tools/code_implementation_server.py
index 41f162bb..d16a9cd5 100644
--- a/tools/code_implementation_server.py
+++ b/tools/code_implementation_server.py
@@ -800,7 +800,7 @@ async def execute_bash(command: str, timeout: int = 30) -> str:
if blocked_reason is not None:
result = {
"status": "error",
- "message": f"Dangerous command blocked ({blocked_reason}): {command}",
+ "message": f"Dangerous command prohibited ({blocked_reason}): {command}",
}
log_operation(
"execute_bash_blocked",验证结果
|
|
thanks for catching this. i pushed the follow-up in the python 3.12/3.13/3.14 and windows jobs that were failing are green now. |
|
Merged on 2026-09-03 as 030810c. Thanks for the quick follow-up on the case handling and the |
Follow-up to #128.
Problem
The
execute_bashguard intools/code_implementation_server.pydecides whether a command is destructive by matching it as a substring of the raw command string:The whitespace-normalization that was already added here helps (
RM -rfno longer slips through), but a normalized substring check is still bypassable, and issue #128 explicitly kept this open to track it:rm -r -f /rm --recursive --force /chmod 0777 filetouch rm-rf-notes.txtSo the layer both misses real destructive commands and false-positives on harmless ones.
Change
Add
core/harness/command_guard.pywithscreen_command(command) -> str | None. It splits the command on the shell control operators (;&&|||&and newlines) and tokenizes each segment withshlex, then matches on the argv (command name + flags) rather than substrings. That:-rf,-fr,-r -f,--recursive --forceas equivalent (flag order / combination / spelling no longer matter);777,0777) and symbolic (a+rwx) chmod forms;echo hi && rm -rf /,cd /tmp; rm -rf build);execute_bashnow callsscreen_commandin place of the substring list.This is defense-in-depth, not the boundary
I have deliberately not touched the sandbox. As #128 concluded, the workspace sandbox in
core.harness.sandboxis the real enforcement; this screen is the cheap first pass in front of it. It never raises, and any command it cannot tokenize (unbalanced quotes, etc.) is passed straight through to the sandbox unchanged — the goal is to make the shallow layer honestly catch what it claims to, without pretending to be the boundary.Tests
tests/test_command_guard.pycovers the historical bypasses, the old false-positives, multi-segment commands, empty input, and unparseable input.ruff checkandruff format --checkare clean on the new and modified files.