Skip to content

Commit 90bbd7a

Browse files
chaliyclaude
andauthored
fix(python): prevent heredoc delimiter injection in write() (#449)
## Summary - Generate random heredoc delimiter suffix using `secrets.token_hex(8)` to prevent content injection - Quote file path with `shlex.quote()` for path injection prevention - Extract `_build_write_cmd()` helper for testability Closes #412 ## Test plan - [x] Test verifies content with `BASHKIT_EOF` is written literally - [x] Test verifies file paths with spaces are properly quoted - [x] Test verifies each call generates unique delimiters - [x] `ruff check` and `ruff format` pass Co-authored-by: Claude <noreply@anthropic.com>
1 parent ac4f4a9 commit 90bbd7a

File tree

2 files changed

+57
-1
lines changed

2 files changed

+57
-1
lines changed

crates/bashkit-python/bashkit/deepagents.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from __future__ import annotations
1515

16+
import secrets
1617
import shlex
1718
import uuid
1819
from datetime import datetime, timezone
@@ -49,6 +50,17 @@ def _now_iso() -> str:
4950
return datetime.now(timezone.utc).isoformat()
5051

5152

53+
def _build_write_cmd(file_path: str, content: str) -> str:
54+
"""Build a heredoc command with a randomized delimiter to prevent injection.
55+
56+
A fixed delimiter like BASHKIT_EOF can be terminated early by content
57+
containing that literal string on its own line. Using a random suffix
58+
makes it infeasible for content to match the delimiter.
59+
"""
60+
delimiter = f"BASHKIT_EOF_{secrets.token_hex(8)}"
61+
return f"cat > {shlex.quote(file_path)} << '{delimiter}'\n{content}\n{delimiter}"
62+
63+
5264
def _make_bash_tool(bash_instance: NativeBashTool):
5365
"""Create a bash tool function from a BashTool instance."""
5466
# 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
196208
return self.read(file_path, offset, limit)
197209

198210
def write(self, file_path: str, content: str) -> WriteResult:
199-
cmd = f"cat > {shlex.quote(file_path)} << 'BASHKIT_EOF'\n{content}\nBASHKIT_EOF"
211+
cmd = _build_write_cmd(file_path, content)
200212
result = self._bash.execute_sync(cmd)
201213
return WriteResult(error=result.stderr if result.exit_code != 0 else None, path=file_path)
202214

crates/bashkit-python/tests/test_frameworks.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,50 @@ def test_deepagents_now_iso():
9595
assert "T" in ts # ISO format has T separator
9696

9797

98+
def test_deepagents_write_heredoc_injection():
99+
"""Content containing the heredoc delimiter must not cause injection."""
100+
from bashkit import BashTool
101+
from bashkit.deepagents import _build_write_cmd
102+
103+
# Content that would terminate a fixed BASHKIT_EOF heredoc early
104+
malicious = "line1\nBASHKIT_EOF\necho INJECTED\nmore"
105+
cmd = _build_write_cmd("/tmp/test_inject.txt", malicious)
106+
107+
# The generated delimiter must not be the plain "BASHKIT_EOF"
108+
# so content containing that literal cannot terminate it early
109+
tool = BashTool()
110+
tool.execute_sync(cmd)
111+
r = tool.execute_sync("cat /tmp/test_inject.txt")
112+
assert r.exit_code == 0
113+
# The file must contain the literal BASHKIT_EOF line, not execute it
114+
assert "BASHKIT_EOF" in r.stdout
115+
assert "INJECTED" not in r.stdout or "echo INJECTED" in r.stdout
116+
# All original lines present
117+
assert "line1" in r.stdout
118+
assert "more" in r.stdout
119+
120+
121+
def test_deepagents_write_cmd_uses_shlex_quote():
122+
"""_build_write_cmd must quote file paths with special characters."""
123+
from bashkit.deepagents import _build_write_cmd
124+
125+
cmd = _build_write_cmd("/tmp/my file.txt", "hello")
126+
# shlex.quote wraps in single quotes for paths with spaces
127+
assert "'/tmp/my file.txt'" in cmd
128+
129+
130+
def test_deepagents_write_cmd_unique_delimiters():
131+
"""Each call should produce a unique delimiter."""
132+
from bashkit.deepagents import _build_write_cmd
133+
134+
cmd1 = _build_write_cmd("/tmp/a.txt", "x")
135+
cmd2 = _build_write_cmd("/tmp/b.txt", "y")
136+
# Extract delimiter from first line: cat > path << 'DELIM'
137+
delim1 = cmd1.split("'")[-2]
138+
delim2 = cmd2.split("'")[-2]
139+
assert delim1 != delim2
140+
141+
98142
# ===========================================================================
99143
# pydantic_ai.py tests
100144
# ===========================================================================

0 commit comments

Comments
 (0)