Command injection payloads are crafted inputs that trick an application into running attacker-chosen operating system commands, and they succeed only when an app builds a shell command from untrusted data without proper separation. This post explains the flaw defensively: how the class works conceptually, how to spot it in your own code, and the fixes that eliminate it. There are no working exploits here against real targets, because the useful knowledge for a defender is understanding the shape of the problem, not weaponizing it.
Why the flaw exists
Command injection happens when an application hands user input to a system shell. Consider a feature that pings a host the user supplies. A naive implementation might concatenate the input directly into a shell string:
# Vulnerable: user input flows straight into a shell
import os
host = request.args.get("host")
os.system("ping -c 1 " + host)
The problem is that os.system invokes a shell, and a shell interprets metacharacters. Characters like ;, |, &&, and backticks let an attacker append or chain additional commands. Because the input is concatenated as text rather than passed as a distinct argument, the shell cannot tell the intended command from the injected one. That is the entire vulnerability in one sentence: the shell loses the boundary between code and data.
The same pattern appears everywhere an app shells out: image processing that calls convert, PDF generation that calls a CLI tool, backup jobs that build tar commands, and countless "run this utility with a user-supplied filename" features.
What payloads look like, conceptually
Attackers probe for command injection with inputs that would be harmless in a value but meaningful to a shell. The categories are worth recognizing so you can spot them in logs and test cases:
- Separators such as
;or newline, which terminate one command and start another - Pipes (
|), which feed one command's output into another - Logical operators (
&&,||), which chain commands conditionally - Command substitution using backticks or
$(), which run a nested command - Blind techniques that produce no visible output, where an attacker infers success through timing (a deliberate delay) or an outbound network callback
The blind variety matters for detection. Just because a response looks normal does not mean the input reached a safe place. An attacker who cannot see output will often trigger a measurable delay or a DNS lookup to confirm the flaw, so your detection has to look beyond the HTTP response body.
Detecting command injection in your code
Static analysis is the most reliable first pass. A SAST-capable tool traces data flow from input sources (request parameters, headers, uploaded filenames) to dangerous sinks (functions that spawn a shell) and flags any path that lacks sanitization in between. Grep-style searches also catch the obvious cases; audit for calls like:
os.system,subprocesswithshell=True,os.popenin PythonRuntime.execwith a concatenated string,ProcessBuildermisuse in Javachild_process.exec(as opposed toexecFile) in Node.js- backticks and
system()in Ruby or Perl
Dynamic testing complements this. A DAST scan submits time-delay and callback-style probes to inputs and watches for the telltale delayed response or out-of-band signal, which catches blind cases that static review might rank as lower confidence.
Prevention that actually works
The fixes here are decisive. Command injection is one of the more preventable flaw classes once you stop invoking a shell.
Avoid the shell entirely. Pass arguments as a list to an exec-style API so the OS treats each element as a discrete argument, never re-parsed by a shell:
# Safe: no shell, input is a distinct argument
import subprocess
host = request.args.get("host")
subprocess.run(["ping", "-c", "1", host], shell=False, check=True)
Here host can contain semicolons all day; there is no shell to interpret them.
Prefer language libraries over shelling out. If you need to make an HTTP request, use an HTTP client, not curl. If you need to manipulate files, use the filesystem API. Every command you do not shell out for is a payload that has nowhere to land.
Validate input against an allowlist. When the input should be one of a known set (a hostname, an ID, a filename from a fixed directory), validate it against a strict pattern or list and reject anything else. Allowlisting is far stronger than trying to blocklist dangerous characters, because blocklists always miss something.
Run with least privilege. If the process that shells out runs as an unprivileged user in a constrained container, a successful injection does far less damage. This is defense in depth, not a substitute for the fixes above.
Where it fits in the bigger picture
Command injection sits in the broader injection family alongside SQL injection and template injection, all of which share the same root cause: mixing untrusted data with a language interpreter. If you fix the pattern once, in code review standards and secure defaults, you tend to fix it everywhere. Bake shell-free execution and input allowlisting into your framework conventions and the flaw largely stops appearing in new code. Practitioner secure-coding modules are available in the Safeguard Academy.
FAQ
Is command injection the same as code injection?
They are related but distinct. Command injection runs operating-system shell commands; code injection runs code in the application's own language (for example, injecting into an eval call). Both stem from mixing untrusted input with an interpreter, and both are prevented by keeping data and code strictly separated.
Does input sanitization stop command injection?
Escaping shell metacharacters can help but is fragile, because shells have many special characters and quoting rules that are easy to get wrong. The reliable fix is to not invoke a shell at all: pass arguments as a discrete list to an exec-style API, so there is no shell to interpret metacharacters in the first place.
How do I find command injection in a large codebase?
Combine static analysis, which traces input from request sources to shell-spawning sinks, with a code audit for shell-invoking functions like subprocess with shell=True, Runtime.exec, and child_process.exec. Add dynamic testing with time-delay and out-of-band probes to catch blind cases that produce no visible output.
Why is "blind" command injection dangerous if there is no output?
Because the command still runs. An attacker who cannot see output can still exfiltrate data over an outbound network connection, plant a backdoor, or pivot deeper into your network. They confirm the flaw through timing delays or DNS callbacks rather than visible responses, which is exactly why detection must look beyond the HTTP response body.