From af582246f141311d574551b7571a517bcc3df750 Mon Sep 17 00:00:00 2001 From: Bobby Date: Tue, 11 Aug 2026 14:16:16 -0400 Subject: [PATCH] [Security] `exec.allowPatterns` shell-chain bypass allows unintended command execution Closes #5306 --- nanobot/agent/tools/shell.py | 4 +++- tests/tools/test_exec_allow_patterns.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/nanobot/agent/tools/shell.py b/nanobot/agent/tools/shell.py index 001be5897..19f1d9f9a 100644 --- a/nanobot/agent/tools/shell.py +++ b/nanobot/agent/tools/shell.py @@ -993,7 +993,9 @@ class ExecTool(Tool): ): current.append(ch) operator_len = 1 - elif ch in {";", "|"}: + # A newline separates commands just like ";" does, so a payload + # smuggled onto its own line must be checked on its own too. + elif ch in {";", "|", "\n", "\r"}: operator_len = 1 if operator_len: diff --git a/tests/tools/test_exec_allow_patterns.py b/tests/tools/test_exec_allow_patterns.py index 0a10d5ec8..2ae3b7716 100644 --- a/tests/tools/test_exec_allow_patterns.py +++ b/tests/tools/test_exec_allow_patterns.py @@ -76,6 +76,31 @@ def test_guard_allow_patterns_block_single_ampersand_chained_segment(): assert "allowlist" in result.lower() +def test_guard_allow_patterns_block_newline_chained_segment(): + """A newline separates commands, so each line must match on its own.""" + tool = ExecTool(allow_patterns=[r"echo\s+allowlisted\s*.*"]) + + result = tool._guard_command("echo allowlisted\ntouch /tmp/evil", "/tmp") + assert result is not None + assert "allowlist" in result.lower() + + +def test_guard_newline_chained_segment_still_hits_deny_patterns(): + """An allowlisted first line does not exempt a denied later line.""" + tool = ExecTool(allow_patterns=[r"echo\s+allowlisted\s*.*"]) + + result = tool._guard_command("echo allowlisted\nrm -rf /", "/tmp") + assert result is not None + assert "deny pattern filter" in result.lower() + + +def test_split_shell_segments_keep_line_continuation_intact(): + """A backslash-escaped newline continues one command, not a new segment.""" + assert ExecTool._split_shell_segments("echo allowlisted \\\nextra") == [ + "echo allowlisted \\\nextra" + ] + + def test_guard_allow_patterns_preserve_trailing_background_operator(): tool = ExecTool(allow_patterns=[r"echo\s+allowlisted"])