Secrets management — .env, never commit credentials, Vault
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 reacheseval(), it's arbitrary PHP execution.preg_replace()with/emodifier (removed in PHP 7.0): Was RCE./emodifier is gone.include(),require(),include_once(),require_once()with user input: Remote file inclusion (ifallow_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
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)