What Is Command Injection? OS Command Injection Explained
A complete technical guide to OS command injection: how untrusted input turns into arbitrary shell execution, and how to detect, verify, and prevent it.
Short answer
OS command injection happens when an application passes user-controlled input into a system shell command, and the shell's own control characters let an attacker chain in commands the application never intended to run.
The idea in one minute
Picture a shop assistant who takes your spoken request and, rather than looking anything up themselves, literally retypes your exact words at a terminal and presses enter. You say "socks," they type lookup socks. It works fine — until you say "socks; delete inventory," and the terminal — which treats ; as "run this command, then run the next one too" — does exactly that. The assistant didn't misbehave. They followed instructions perfectly. The terminal just never distinguished between "the thing you're looking up" and "a second command you're also issuing."
That's command injection. The "terminal" is a system shell; the "assistant" is application code that builds a command string from user input and hands it to that shell. Shells have always supported chaining (;, &&, |) and substitution (` `, $()) — that's a feature, not a bug, for legitimate shell scripting. The vulnerability is letting untrusted input reach that shell at all.
How command injection actually works
Application code often shells out to existing command-line tools instead of reimplementing their functionality — running ping, converting an image, checking a DNS record. When that command is built by string concatenation and executed through a shell, every shell metacharacter in the input is interpreted by the shell, not treated as a literal value.
| Category | What happens | How it's confirmed |
|---|---|---|
| Classic/in-band | Injected command's output appears directly in the response | Output visible on-page |
| Blind | No output returned, but the command still executes | Time-delay (sleep) or out-of-band callback |
| Argument injection | Can't chain a new command, but can inject additional flags/arguments into the fixed command | Behavior change from an unexpected flag (e.g., pointing an output path somewhere new) |
The request flow
sequenceDiagram
participant Attacker
participant App as Application
participant Shell as OS Shell
Attacker->>App: Input containing shell metacharacters
App->>Shell: Concatenated command string
Shell-->>App: Executes BOTH the intended command and the injected one
App-->>Attacker: Output reflected, or effects observed out-of-band
A minimal example
A "ping this host" diagnostic feature:
import os
from flask import request
@app.route("/ping")
def ping():
host = request.args.get("host")
output = os.popen(f"ping -c 4 {host}").read() # shell interprets metacharacters
return output
A request with host=8.8.8.8; cat /etc/passwd runs the ping and dumps the password file into the response, because the shell sees two commands separated by ;.
The fix isn't sanitizing every dangerous character — it's not invoking a shell at all:
import subprocess
import re
from flask import request, abort
@app.route("/ping")
def ping():
host = request.args.get("host")
if not re.match(r'^[a-zA-Z0-9.\-]+$', host):
abort(400)
result = subprocess.run(["ping", "-c", "4", host], capture_output=True, text=True)
return result.stdout
Passing arguments as a list to subprocess.run (without shell=True) sends them directly to the ping program — there's no shell in between to interpret ;, |, or ` `, so those characters are just inert text as far as ping is concerned. The allowlist regex adds a second layer on top.
Why the vulnerability exists
- Application code shells out to CLI utilities for convenience instead of using native library equivalents
- APIs like
os.system,os.popen, orsubprocess.run(..., shell=True)are used, all of which invoke a shell that parses metacharacters - Commands are built via string concatenation instead of passed as argument arrays
- Input that "looks like" a hostname, filename, or simple identifier is assumed safe without strict validation
What attackers look for
- Diagnostic or utility features that clearly shell out: ping/traceroute/DNS lookup tools, "test this connection" buttons
- Media processing pipelines that wrap external binaries (image conversion, video transcoding, PDF generation)
- File compression/extraction features
- Any endpoint whose behavior strongly resembles "take this input and run a system tool against it"
Detection
- Static/code review: search for shell-invoking calls (
system(),popen(),exec(),subprocesswithshell=True, backticks) fed by request data without a strict allowlist. - Dynamic/DAST: for visible-output endpoints, inject a benign marker command (e.g., appending
; echo <unique-string>) and check whether it appears in the response; for blind cases, inject a time-delay command (; sleep 6) and measure response time against a baseline. - Out-of-band testing: where no output or timing signal is available, trigger a DNS/HTTP callback via an injected command to infrastructure you control.
Verification: real vulnerability or false positive?
Confirmed when the injected metacharacters produce a result that can only be explained by shell execution:
- Command output appears that matches the injected marker, not the intended command's normal output
- A time-based payload produces a delay matching the specified duration, repeatably, with a control request (no delay clause) showing no such delay
- An out-of-band interaction is received, uniquely tied to the specific payload sent
A single slow response is not evidence — network jitter and server load produce the same symptom. Repeat the timing test and compare against an unmodified baseline before calling it confirmed.
Real-world impact
Command injection converts directly into remote code execution as the application's process user — there's no additional chaining required, which is why it's consistently ranked among the most dangerous vulnerability classes by organizations like CISA and MITRE. Consequences typically include full server compromise, credential theft from the compromised host, lateral movement into internal networks, and persistence (planting a backdoor) — the ceiling is "whatever the application's OS user account is allowed to do."
Prevention
- Avoid invoking a shell entirely: use library APIs or
subprocess-style calls with argument arrays instead of shell command strings — this removes the shell's metacharacter interpretation from the equation completely - Hardcode the command and its required flags: never let user input choose which executable runs or which required options are passed
- Strict allowlist validation on any input that must reach a command: only permit the specific character set a legitimate value could contain (e.g., a hostname pattern)
- Least privilege: run the process under an account with minimal OS permissions, so a successful injection has a limited blast radius
- Keep dependencies current: some injection paths come through vulnerable third-party libraries that shell out internally, not just first-party code
Related vulnerabilities
- SQL injection — the same "user input becomes executable syntax" root cause, applied to a database query language instead of a shell
- Server-Side Template Injection (SSTI) — another interpreter-confusion vulnerability class
- Path traversal — often chained alongside command injection to reach or overwrite specific files
- Argument injection — the narrower case where new arguments, not new commands, get injected into a fixed executable
Testing methodology (do this safely)
- Only test targets you're authorized to test.
- Prefer time-based blind confirmation (
sleep) over commands that read, modify, or exfiltrate real data. - Never run destructive commands (file deletion, service disruption) outside an explicitly authorized, isolated environment.
- Document the exact payload, and the output or timing evidence that confirms execution.
Further reading
- OWASP: Command Injection
- OWASP Cheat Sheet Series: OS Command Injection Defense Cheat Sheet
- PortSwigger Web Security Academy: OS command injection
- MITRE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command
Nyxeara perspective
Blind command injection is only as trustworthy as its timing evidence. A scanner reporting a finding off a single slow response, without a repeated measurement against a clean baseline, will produce exactly the false positives that make automated results hard to trust — the sleep duration has to show up reliably, tied to the specific payload, before it counts as confirmed.