0

Secrets management — .env, never commit credentials, Vault

Beginner5 min read·php-16-008
security

Concept

Remote Code Execution (RCE) and command injection vulnerabilities allow attackers to execute arbitrary code or OS commands on the server. These are among the most severe vulnerabilities — they give attackers full server access.

PHP functions that execute OS commands — never pass user input to these without a strict allowlist or proper escaping:

  • exec(), shell_exec(), `command` (backtick operator), passthru(), popen(), system(), proc_open().
  • eval(): Executes PHP code string — if user input reaches eval(), it's arbitrary PHP execution.
  • preg_replace() with /e modifier (removed in PHP 7.0): Was RCE. /e modifier is gone.
  • include(), require(), include_once(), require_once() with user input: Remote file inclusion (if allow_url_include = On) or local file inclusion (LFI).

escapeshellarg(string $arg): Escapes and quotes a string for use as a shell argument. Wraps the value in single quotes and escapes any single quotes within. Use for arguments.

escapeshellcmd(string $command): Escapes characters that have special meaning in shell commands. Use for the command itself (rarely needed — prefer escapeshellarg for arguments).

Better approach: Use libraries that don't invoke a shell: use PHP's built-in functions for file operations, use HTTP client for network requests, use PDO for database. If you must call external programs, use proc_open() with the command as an array (no shell interpolation) in PHP 8.0+, or symfony/process.

Code Example

php
<?php
declare(strict_types=1);

// VULNERABLE — command injection
$filename = $_GET['file']; // attacker sends: "report.pdf; rm -rf /"
exec("ls -la /uploads/$filename"); // EXECUTES: ls -la /uploads/report.pdf; rm -rf /

// SAFE — escapeshellarg for user input
$filename = basename($_GET['file'] ?? ''); // strip path components first
if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $filename)) {
    throw new \InvalidArgumentException("Invalid filename");
}
$escaped = escapeshellarg("/uploads/$filename");
exec("ls -la $escaped");

// BETTER — use PHP functions instead of shell commands
$files = scandir('/uploads/'); // no shell — pure PHP

// symfony/process — safe command execution (array form avoids shell)
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;

$process = new Process(['ffmpeg', '-i', $inputFile, '-vf', 'scale=720:-1', $outputFile]);
$process->setTimeout(60);
$process->run();
if (!$process->isSuccessful()) {
    throw new ProcessFailedException($process);
}

// VULNERABLE — eval with user input
eval($_GET['code']); // DO NOT DO THIS EVER

// VULNERABLE — Local File Inclusion (LFI)
$page = $_GET['page']; // attacker sends: "../../etc/passwd"
include "/var/www/pages/$page.php"; // VULNERABLE

// SAFE — allowlist for LFI
$allowed = ['home', 'about', 'contact'];
$page = $_GET['page'] ?? 'home';
if (!in_array($page, $allowed, strict: true)) {
    $page = 'home';
}
include "/var/www/pages/$page.php"; // only allowed values

// Disable dangerous functions in php.ini (production hardening)
// disable_functions = exec,passthru,shell_exec,system,popen,proc_open,eval,base64_decode
// (base64_decode inclusion is controversial — may break legitimate use)