Command injection happens when a program builds a shell command by string concatenation and asks the operating system to run it. The shell is a programming language — it has its own parser, its own quoting rules, its own metacharacters (;, &&, |, backticks, $(), glob expansion). If a user can sneak any of those into the command, they can execute arbitrary code in the security context of your process.

The fix is to bypass the shell entirely. Most modern languages have a way to invoke a subprocess as an array of arguments rather than a single shell string: execve directly, or subprocess.run([...], shell=False), or child_process.spawn('cmd', [...]). The kernel hands the arguments to the target program with no shell in the loop, and metacharacters become inert.

Where this still bites: scripts that need a small bit of shell glue (pipes, redirection) and reach for shell=True for convenience. The right answer is almost always to do that orchestration in your program (open files yourself, write your own pipes) instead. Treat shell=True like unsafe { ... } in Rust — sometimes you must, but each instance should be justified.

Review heuristic

If a string built from a request flows into a function whose name involves the words shell, system, exec, popen, or eval, treat it as actively dangerous until you can show that no part of the string is attacker-controlled.

External reference

CWE-78: OS Command Injection — the canonical industry classification for this bug class. Useful when filing tickets, writing security policies, or arguing with a static analyzer.