Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion crates/bashkit-python/bashkit/deepagents.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from __future__ import annotations

import secrets
import shlex
import uuid
from datetime import datetime, timezone
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
44 changes: 44 additions & 0 deletions crates/bashkit-python/tests/test_frameworks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ===========================================================================
Expand Down
Loading