From 9ff5ec9b8070d7c33093bb7bd495a34f9f5ad7a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Mar 2026 08:39:22 +0000 Subject: [PATCH] fix(python): prevent heredoc delimiter injection in write() https://claude.ai/code/session_01WZjYqxm5xMPAEe7FSHJkDy --- crates/bashkit-python/bashkit/deepagents.py | 14 +++++- .../bashkit-python/tests/test_frameworks.py | 44 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/crates/bashkit-python/bashkit/deepagents.py b/crates/bashkit-python/bashkit/deepagents.py index 211da4cb..a6577cb7 100644 --- a/crates/bashkit-python/bashkit/deepagents.py +++ b/crates/bashkit-python/bashkit/deepagents.py @@ -13,6 +13,7 @@ from __future__ import annotations +import secrets import shlex import uuid from datetime import datetime, timezone @@ -49,6 +50,17 @@ def _now_iso() -> str: return datetime.now(timezone.utc).isoformat() +def _build_write_cmd(file_path: str, content: str) -> str: + """Build a heredoc command with a randomized delimiter to prevent injection. + + A fixed delimiter like BASHKIT_EOF can be terminated early by content + containing that literal string on its own line. Using a random suffix + makes it infeasible for content to match the delimiter. + """ + delimiter = f"BASHKIT_EOF_{secrets.token_hex(8)}" + return f"cat > {shlex.quote(file_path)} << '{delimiter}'\n{content}\n{delimiter}" + + def _make_bash_tool(bash_instance: NativeBashTool): """Create a bash tool function from a BashTool instance.""" # Use name and description from bashkit lib @@ -196,7 +208,7 @@ async def aread(self, file_path: str, offset: int = 0, limit: int = 2000) -> str return self.read(file_path, offset, limit) def write(self, file_path: str, content: str) -> WriteResult: - cmd = f"cat > {shlex.quote(file_path)} << 'BASHKIT_EOF'\n{content}\nBASHKIT_EOF" + cmd = _build_write_cmd(file_path, content) result = self._bash.execute_sync(cmd) return WriteResult(error=result.stderr if result.exit_code != 0 else None, path=file_path) diff --git a/crates/bashkit-python/tests/test_frameworks.py b/crates/bashkit-python/tests/test_frameworks.py index 2c62490d..94cbc426 100644 --- a/crates/bashkit-python/tests/test_frameworks.py +++ b/crates/bashkit-python/tests/test_frameworks.py @@ -95,6 +95,50 @@ def test_deepagents_now_iso(): assert "T" in ts # ISO format has T separator +def test_deepagents_write_heredoc_injection(): + """Content containing the heredoc delimiter must not cause injection.""" + from bashkit import BashTool + from bashkit.deepagents import _build_write_cmd + + # Content that would terminate a fixed BASHKIT_EOF heredoc early + malicious = "line1\nBASHKIT_EOF\necho INJECTED\nmore" + cmd = _build_write_cmd("/tmp/test_inject.txt", malicious) + + # The generated delimiter must not be the plain "BASHKIT_EOF" + # so content containing that literal cannot terminate it early + tool = BashTool() + tool.execute_sync(cmd) + r = tool.execute_sync("cat /tmp/test_inject.txt") + assert r.exit_code == 0 + # The file must contain the literal BASHKIT_EOF line, not execute it + assert "BASHKIT_EOF" in r.stdout + assert "INJECTED" not in r.stdout or "echo INJECTED" in r.stdout + # All original lines present + assert "line1" in r.stdout + assert "more" in r.stdout + + +def test_deepagents_write_cmd_uses_shlex_quote(): + """_build_write_cmd must quote file paths with special characters.""" + from bashkit.deepagents import _build_write_cmd + + cmd = _build_write_cmd("/tmp/my file.txt", "hello") + # shlex.quote wraps in single quotes for paths with spaces + assert "'/tmp/my file.txt'" in cmd + + +def test_deepagents_write_cmd_unique_delimiters(): + """Each call should produce a unique delimiter.""" + from bashkit.deepagents import _build_write_cmd + + cmd1 = _build_write_cmd("/tmp/a.txt", "x") + cmd2 = _build_write_cmd("/tmp/b.txt", "y") + # Extract delimiter from first line: cat > path << 'DELIM' + delim1 = cmd1.split("'")[-2] + delim2 = cmd2.split("'")[-2] + assert delim1 != delim2 + + # =========================================================================== # pydantic_ai.py tests # ===========================================================================