PHP Security Patterns
Language-level security patterns for PHP, applicable to any PHP project.
Input Validation
Type Validation
// Always validate and sanitize input public function processUserId(mixed $input): int { if (!is_numeric($input)) { throw new \InvalidArgumentException('User ID must be numeric'); }
$id = (int)$input;
if ($id <= 0) {
throw new \InvalidArgumentException('User ID must be positive');
}
return $id;
}
Email Validation
public function validateEmail(string $email): bool { // Use built-in filter $filtered = filter_var($email, FILTER_VALIDATE_EMAIL);
if ($filtered === false) {
throw new \InvalidArgumentException('Invalid email format');
}
return true;
}
URL Validation
public function validateUrl(string $url): bool { $filtered = filter_var($url, FILTER_VALIDATE_URL);
if ($filtered === false) {
throw new \InvalidArgumentException('Invalid URL format');
}
// Additional checks
$parsed = parse_url($url);
// Only allow http/https
if (!in_array($parsed['scheme'] ?? '', ['http', 'https'])) {
throw new \InvalidArgumentException('URL must use http or https');
}
return true;
}
SQL Injection Prevention
Use ORM Query Builder (Recommended)
// ✅ CORRECT: Use ORM (CakePHP example) public function findUserByEmail(string $email): ?User { return $this->Users->find() ->where(['email' => $email]) // Automatically parameterized ->first(); }
// ✅ CORRECT: ORM with conditions array public function findActiveUsers(int $companyId): array { return $this->Users->find() ->where([ 'company_id' => $companyId, 'status' => 1, 'del_flg' => 0, ]) ->toArray(); }
// ❌ WRONG: Raw SQL with string concatenation public function findUserUnsafe(string $email): ?User { $query = "SELECT * FROM users WHERE email = '" . $email . "'"; // VULNERABLE! return $this->getConnection()->query($query)->fetch(); }
Use Query Expressions for Complex Conditions
// ✅ CORRECT: Use Query expressions for complex logic public function findUsersWithComplexCondition(string $searchTerm): array { return $this->Users->find() ->where(function ($exp, $q) use ($searchTerm) { return $exp->or([ 'email LIKE' => '%' . $searchTerm . '%', 'name LIKE' => '%' . $searchTerm . '%', ]); }) ->toArray(); }
Only Use Raw SQL When Absolutely Necessary
// If raw SQL is unavoidable, ALWAYS use parameter binding public function executeRawQuery(int $userId): array { $conn = $this->getConnection();
// ✅ CORRECT: Named parameters
$stmt = $conn->execute(
'SELECT * FROM users WHERE id = :id',
['id' => $userId],
['id' => 'integer']
);
return $stmt->fetchAll();
}
// ❌ WRONG: Never concatenate user input public function unsafeRawQuery(string $email): array { $sql = "SELECT * FROM users WHERE email = '$email'"; // VULNERABLE! return $this->getConnection()->execute($sql)->fetchAll(); }
XSS (Cross-Site Scripting) Prevention
Output Escaping
// ✅ CORRECT: Escape output echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
// For HTML attributes echo '<input value="' . htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . '">';
// For JavaScript echo '<script>var name = "' . json_encode($name, JSON_HEX_TAG | JSON_HEX_AMP) . '";</script>';
// For URLs echo '<a href="' . urlencode($url) . '">Link</a>';
Content Security Policy
// Set CSP headers header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
CSRF (Cross-Site Request Forgery) Prevention
Token Generation
class CsrfToken { public static function generate(): string { if (session_status() === PHP_SESSION_NONE) { session_start(); }
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
return $token;
}
public static function validate(string $token): bool
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
}
Form Implementation
// In form <form method="POST"> <input type="hidden" name="csrf_token" value="<?= CsrfToken::generate() ?>"> <!-- Other fields --> </form>
// In handler if (!CsrfToken::validate($_POST['csrf_token'] ?? '')) { throw new SecurityException('Invalid CSRF token'); }
Password Security
Hashing
// ✅ CORRECT: Use password_hash() public function hashPassword(string $password): string { return password_hash($password, PASSWORD_ARGON2ID, [ 'memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3, ]); }
// ❌ WRONG: Never use md5, sha1, or plain text public function insecureHash(string $password): string { return md5($password); // VULNERABLE! }
Verification
public function verifyPassword(string $password, string $hash): bool { return password_verify($password, $hash); }
public function needsRehash(string $hash): bool { return password_needs_rehash($hash, PASSWORD_ARGON2ID, [ 'memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3, ]); }
Session Security
Secure Session Configuration
// Configure secure sessions ini_set('session.cookie_httponly', '1'); ini_set('session.cookie_secure', '1'); // HTTPS only ini_set('session.cookie_samesite', 'Strict'); ini_set('session.use_strict_mode', '1');
// Regenerate session ID on login session_start(); if ($loginSuccessful) { session_regenerate_id(true); }
Session Fixation Prevention
public function login(string $username, string $password): bool { if ($this->authenticate($username, $password)) { // Regenerate session ID to prevent fixation session_regenerate_id(true);
$_SESSION['user_id'] = $userId;
$_SESSION['last_activity'] = time();
return true;
}
return false;
}
Session Timeout
public function checkSessionTimeout(): bool { $timeout = 1800; // 30 minutes
if (isset($_SESSION['last_activity'])) {
if (time() - $_SESSION['last_activity'] > $timeout) {
session_destroy();
return false;
}
}
$_SESSION['last_activity'] = time();
return true;
}
File Upload Security
Validation
public function validateUpload(array $file): bool { // Check for upload errors if ($file['error'] !== UPLOAD_ERR_OK) { throw new \RuntimeException('Upload failed'); }
// Validate MIME type
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
throw new \InvalidArgumentException('Invalid file type');
}
// Validate file size (max 5MB)
if ($file['size'] > 5 * 1024 * 1024) {
throw new \InvalidArgumentException('File too large');
}
return true;
}
public function saveUpload(array $file): string { $this->validateUpload($file);
// Generate safe filename
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = bin2hex(random_bytes(16)) . '.' . $extension;
// Save outside web root
$uploadDir = '/var/uploads/';
$destination = $uploadDir . $filename;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new \RuntimeException('Failed to save file');
}
return $filename;
}
Directory Traversal Prevention
Path Sanitization
public function getSecurePath(string $userPath): string { // Define safe base directory $baseDir = realpath('/var/www/uploads/');
// Resolve user path
$requestedPath = realpath($baseDir . '/' . $userPath);
// Ensure path is within base directory
if ($requestedPath === false || strpos($requestedPath, $baseDir) !== 0) {
throw new \InvalidArgumentException('Invalid path');
}
return $requestedPath;
}
Command Injection Prevention
Avoid shell execution
// ✅ CORRECT: Use native PHP functions $files = scandir($directory);
// ✅ CORRECT: If shell needed, use escapeshellarg() $filename = escapeshellarg($userFilename); exec("ls -la " . $filename, $output);
// ❌ WRONG: Direct shell execution with user input exec("ls -la " . $userFilename); // VULNERABLE!
XML External Entity (XXE) Prevention
Disable external entities
// ✅ CORRECT: Disable external entity loading libxml_disable_entity_loader(true);
$dom = new DOMDocument(); $dom->loadXML($xmlString, LIBXML_NOENT | LIBXML_DTDLOAD);
// Or use SimpleXML $xml = simplexml_load_string($xmlString, 'SimpleXMLElement', LIBXML_NOENT);
Cryptographic Random Numbers
Use secure random
// ✅ CORRECT: Use random_bytes() or random_int() $token = bin2hex(random_bytes(32)); $randomNumber = random_int(1, 100);
// ❌ WRONG: Never use rand() or mt_rand() for security $insecureToken = mt_rand(); // VULNERABLE!
HTTP Headers
Security Headers
// X-Frame-Options header('X-Frame-Options: SAMEORIGIN');
// X-Content-Type-Options header('X-Content-Type-Options: nosniff');
// X-XSS-Protection header('X-XSS-Protection: 1; mode=block');
// Strict-Transport-Security (HTTPS only) header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
// Referrer-Policy header('Referrer-Policy: strict-origin-when-cross-origin');
Error Handling
Don't expose sensitive information
// ✅ CORRECT: Log detailed errors, show generic message try { $this->processPayment($amount); } catch (\Exception $e) { // Log full error for debugging error_log('Payment error: ' . $e->getMessage());
// Show generic message to user
throw new \RuntimeException('Payment processing failed');
}
// ❌ WRONG: Expose stack traces in production ini_set('display_errors', '1'); // VULNERABLE in production!
Framework-Agnostic
These security patterns apply to:
-
CakePHP projects
-
Laravel projects
-
Symfony projects
-
Any PHP application
Framework-specific security features (Authentication, Authorization, CSRF middleware, etc.) should be defined in framework-level skills (e.g., php-cakephp/security-patterns ).