PHP Security Best Practices: A Complete Guide
Reviewed & published by Brayan K
Master SQL injection prevention, XSS protection, CSRF tokens, password security, and file upload safety in PHP.
11-Minute Read — Master SQL injection prevention, XSS protection, CSRF tokens, password security & more
PHP powers more than 75% of the web, including WordPress, Facebook (initially), Wikipedia, and countless custom applications. But because it's so widely used — it's also one of the most targeted languages by attackers.
If you're building websites, APIs, dashboards, or login systems in PHP, understanding security isn't an option… It's mandatory.
This 11-minute guide will teach you the most common PHP vulnerabilities and — more importantly — how to protect every project you build.
Why PHP Security Matters
PHP often handles the most sensitive parts of a website:
- ✅ Login systems
- ✅ Form submissions
- ✅ User accounts
- ✅ Database interactions
- ✅ Payment flows
- ✅ Admin dashboards
Hackers know this — so they specifically target badly written PHP code.
Common consequences of poor security:
- ❌ Database leaks
- ❌ Stolen passwords
- ❌ User session hijacking
- ❌ Website defacement
- ❌ Full server compromise
The good news: Most attacks are preventable with the right techniques.
1. SQL Injection (SQLi) — The #1 Vulnerability
SQL Injection is the most common PHP vulnerability. It happens when a hacker injects malicious SQL into your queries.
A hacker can enter: admin' OR '1'='1
And instantly bypass your login system.
Prepared statements separate SQL from data, making injection impossible.
<?php
// ❌ VULNERABLE - SQL Injection
$username = $_GET['user'];
$query = "SELECT * FROM users WHERE username = '$username'";
$result = mysqli_query($conn, $query);
// Attacker input: admin' OR '1'='1
// Results in: SELECT * FROM users WHERE username = 'admin' OR '1'='1'
// ✅ SECURE - Prepared Statements (PDO)
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
// ✅ SECURE - Prepared Statements (MySQLi)
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
?>Never build SQL queries by concatenating strings with user input. Always use prepared statements with parameter binding.
2. Cross-Site Scripting (XSS)
XSS occurs when attackers inject malicious JavaScript into your pages through:
- Comment boxes
- Message boards
If your page displays this without escaping, the attacker controls the browser.
<?php
// ❌ VULNERABLE - XSS Attack
echo "Welcome, " . $_GET['name'];
// Attacker input: <script>alert('Hacked!')</script>
// ✅ SECURE - Escape Output
echo "Welcome, " . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
// ✅ SECURE - Function for consistent escaping
function escape($str) {
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
echo "Welcome, " . escape($_GET['name']);
// For HTML attributes
echo '<div data-user="' . escape($_GET['name']) . '">';
// For JavaScript context
echo '<script>var name = "' . json_encode($_GET['name']) . '";</script>';
?>Always escape output — NEVER trust user input. Use htmlspecialchars() for anything displayed on the page.
3. Cross-Site Request Forgery (CSRF)
CSRF tricks a logged-in user into performing actions without consent.
Example: An attacker sends a hidden form that deletes a user account or changes settings.
<?php
// ✅ CSRF Protection Implementation
// 1. Generate CSRF token
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// 2. Add token to forms
?>
<form method="POST" action="/update-profile">
<input type="hidden" name="csrf_token"
value="<?php echo $_SESSION['csrf_token']; ?>">
<input type="text" name="email">
<button type="submit">Update</button>
</form>
<?php
// 3. Verify token on POST
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_POST['csrf_token']) ||
!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
die("Invalid CSRF token!");
}
// Process form...
}
// 4. Helper function for CSRF protection
function verifyCsrfToken() {
if (empty($_POST['csrf_token']) ||
!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
http_response_code(403);
die("CSRF validation failed");
}
}
?>- Generate a random token and store it in the session
- Add the token as a hidden field in all forms
- Verify the token matches on POST requests
- Use hash_equals() for timing-attack-safe comparison
This blocks 99% of CSRF attacks.
4. Password Security
- MD5, SHA1, SHA256 = NOT for passwords
- They're fast and easily brute-forced
- No salt = rainbow table attacks
<?php
// ❌ NEVER DO THIS
$hashed = md5($password); // Weak!
$hashed = sha1($password); // Weak!
$hashed = hash('sha256', $password); // Still weak for passwords!
// ✅ SECURE Password Hashing
// Registration
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_DEFAULT);
// Store $hash in database
$stmt = $pdo->prepare("INSERT INTO users (username, password_hash) VALUES (?, ?)");
$stmt->execute([$username, $hash]);
// Login verification
$stmt = $pdo->prepare("SELECT password_hash FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
// Password correct - log user in
$_SESSION['user_id'] = $user['id'];
// Check if rehash needed (algorithm improved)
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
// Update database with $newHash
}
} else {
// Invalid credentials
echo "Invalid username or password";
}
// Additional: Password strength validation
function validatePasswordStrength($password) {
$errors = [];
if (strlen($password) < 8) {
$errors[] = "Password must be at least 8 characters";
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = "Password must contain uppercase letter";
}
if (!preg_match('/[a-z]/', $password)) {
$errors[] = "Password must contain lowercase letter";
}
if (!preg_match('/[0-9]/', $password)) {
$errors[] = "Password must contain a number";
}
return $errors;
}
?>- ✅ Use password_hash() with PASSWORD_DEFAULT
- ✅ Automatically uses bcrypt or Argon2 (secure algorithms)
- ✅ Includes automatic salting
- ✅ Use password_verify() for checking
- ✅ Check for rehashing with password_needs_rehash()
5. File Upload Vulnerabilities
Allowing users to upload files is extremely dangerous.
- ❌ .php shells (remote access)
- ❌ Scripts disguised as images
<?php
// ✅ SECURE File Upload Implementation
function secureFileUpload($file) {
$errors = [];
// 1. Check if file was uploaded
if (!isset($file) || $file['error'] !== UPLOAD_ERR_OK) {
return ['error' => 'File upload failed'];
}
// 2. Validate file size (5MB limit)
$maxSize = 5 * 1024 * 1024;
if ($file['size'] > $maxSize) {
return ['error' => 'File too large (max 5MB)'];
}
// 3. Validate MIME type (not just extension!)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
$allowedTypes = [
'image/jpeg',
'image/png',
'image/gif',
'application/pdf'
];
if (!in_array($mimeType, $allowedTypes)) {
return ['error' => 'Invalid file type'];
}
// 4. Generate safe filename (never trust original name)
$extension = match($mimeType) {
'image/jpeg' => '.jpg',
'image/png' => '.png',
'image/gif' => '.gif',
'application/pdf' => '.pdf',
default => ''
};
$newFilename = uniqid('upload_', true) . $extension;
// 5. Store outside web root if possible
$uploadDir = __DIR__ . '/../uploads/'; // Outside public/
// Create directory if doesn't exist
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$destination = $uploadDir . $newFilename;
// 6. Move file
if (move_uploaded_file($file['tmp_name'], $destination)) {
return [
'success' => true,
'filename' => $newFilename,
'path' => $destination
];
}
return ['error' => 'Failed to save file'];
}
// Usage
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['upload'])) {
$result = secureFileUpload($_FILES['upload']);
if (isset($result['success'])) {
// Store filename in database
$stmt = $pdo->prepare("INSERT INTO uploads (filename, user_id) VALUES (?, ?)");
$stmt->execute([$result['filename'], $_SESSION['user_id']]);
echo "File uploaded successfully";
} else {
echo $result['error'];
}
}
// Serving uploaded files securely
function serveFile($fileId) {
// Verify user has permission to access this file
$stmt = $pdo->prepare("SELECT filename FROM uploads WHERE id = ? AND user_id = ?");
$stmt->execute([$fileId, $_SESSION['user_id']]);
$file = $stmt->fetch();
if (!$file) {
http_response_code(404);
die("File not found");
}
$filepath = __DIR__ . '/../uploads/' . $file['filename'];
if (!file_exists($filepath)) {
http_response_code(404);
die("File not found");
}
// Set proper headers
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($filepath) . '"');
header('Content-Length: ' . filesize($filepath));
readfile($filepath);
exit;
}
?>File Upload Security Checklist:
- ✅ Validate MIME type (not just extension)
- ✅ Check file size limits
- ✅ Rename uploaded files (never trust original name)
- ✅ Store uploads outside web root when possible
- ✅ Serve files through PHP with permission checks
- ✅ Use whitelist for allowed file types
6. Session Security & Hijacking Prevention
Attackers try to steal user sessions by predicting session IDs, injecting malicious cookies, or forcing users to use known session IDs.
<?php
// ✅ SECURE Session Configuration
// 1. Configure secure session settings BEFORE session_start()
ini_set('session.cookie_httponly', 1); // Prevent JavaScript access
ini_set('session.cookie_secure', 1); // HTTPS only
ini_set('session.cookie_samesite', 'Strict'); // CSRF protection
ini_set('session.use_strict_mode', 1); // Reject uninitialized session IDs
// Or use session_set_cookie_params
session_set_cookie_params([
'lifetime' => 0, // Session cookie (expires on browser close)
'path' => '/',
'domain' => '',
'secure' => true, // HTTPS only
'httponly' => true, // No JavaScript access
'samesite' => 'Strict' // CSRF protection
]);
session_start();
// 2. Regenerate session ID on login
function loginUser($userId) {
// Regenerate session ID to prevent fixation
session_regenerate_id(true);
$_SESSION['user_id'] = $userId;
$_SESSION['login_time'] = time();
$_SESSION['last_activity'] = time();
// Optional: Bind session to user agent and IP
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
$_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
}
// 3. Validate session on each request
function validateSession() {
// Check if session exists
if (!isset($_SESSION['user_id'])) {
return false;
}
// Session timeout (30 minutes of inactivity)
if (isset($_SESSION['last_activity']) &&
(time() - $_SESSION['last_activity'] > 1800)) {
session_destroy();
return false;
}
// Validate user agent (detect session hijacking)
if ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
session_destroy();
return false;
}
// Update last activity
$_SESSION['last_activity'] = time();
return true;
}
// 4. Secure logout
function logoutUser() {
// Clear session data
$_SESSION = array();
// Delete session cookie
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time() - 3600, '/');
}
// Destroy session
session_destroy();
// Redirect to login
header('Location: /login.php');
exit;
}
// Usage in protected pages
if (!validateSession()) {
header('Location: /login.php');
exit;
}
?>- ✅ Regenerate session ID after login
- ✅ Use httponly cookies (prevent JavaScript access)
- ✅ Use secure cookies (HTTPS only)
- ✅ Set SameSite cookie attribute (CSRF protection)
- ✅ Implement session timeout
- ✅ Validate user agent and IP (optional, for high security)
- ✅ Destroy session properly on logout
7. Server-Side Validation (Not Just Client-Side)
Client-side validation (JavaScript) is NOT security. Hackers can bypass it easily by disabling JavaScript or sending direct HTTP requests.
<?php
// ✅ COMPREHENSIVE Input Validation
// 1. Email validation
function validateEmail($email) {
$email = filter_var($email, FILTER_SANITIZE_EMAIL);
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return ['valid' => false, 'error' => 'Invalid email format'];
}
// Optional: Check if domain has MX record
list($user, $domain) = explode('@', $email);
if (!checkdnsrr($domain, 'MX')) {
return ['valid' => false, 'error' => 'Email domain does not exist'];
}
return ['valid' => true, 'email' => $email];
}
// 2. URL validation
function validateUrl($url) {
$url = filter_var($url, FILTER_SANITIZE_URL);
if (!filter_var($url, FILTER_VALIDATE_URL)) {
return false;
}
// Optional: Only allow specific protocols
$parsed = parse_url($url);
if (!in_array($parsed['scheme'] ?? '', ['http', 'https'])) {
return false;
}
return $url;
}
// 3. Integer validation
function validateInt($value, $min = null, $max = null) {
$value = filter_var($value, FILTER_VALIDATE_INT);
if ($value === false) {
return false;
}
if ($min !== null && $value < $min) {
return false;
}
if ($max !== null && $value > $max) {
return false;
}
return $value;
}
// 4. String validation
function validateString($str, $minLen = 1, $maxLen = 255) {
$str = trim($str);
$len = mb_strlen($str);
if ($len < $minLen || $len > $maxLen) {
return false;
}
return $str;
}
// 5. Prevent directory traversal
function securePath($filename) {
// Remove any directory traversal attempts
$filename = str_replace(['../', '..\\'], '', $filename);
$filename = basename($filename);
return $filename;
}
// 6. Whitelist validation (most secure)
function validatePage($page) {
$allowedPages = ['home', 'about', 'contact', 'products'];
if (!in_array($page, $allowedPages, true)) {
return 'home'; // Default safe value
}
return $page;
}
// 7. Complete form validation example
function validateContactForm($data) {
$errors = [];
$clean = [];
// Name validation
if (empty($data['name'])) {
$errors['name'] = 'Name is required';
} elseif (!preg_match('/^[a-zA-Z\s]{2,50}$/', $data['name'])) {
$errors['name'] = 'Name must be 2-50 letters only';
} else {
$clean['name'] = validateString($data['name'], 2, 50);
}
// Email validation
$emailResult = validateEmail($data['email'] ?? '');
if (!$emailResult['valid']) {
$errors['email'] = $emailResult['error'];
} else {
$clean['email'] = $emailResult['email'];
}
// Message validation
if (empty($data['message'])) {
$errors['message'] = 'Message is required';
} else {
$clean['message'] = validateString($data['message'], 10, 1000);
if (!$clean['message']) {
$errors['message'] = 'Message must be 10-1000 characters';
}
}
return [
'valid' => empty($errors),
'errors' => $errors,
'data' => $clean
];
}
// Usage
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = validateContactForm($_POST);
if ($result['valid']) {
// Process clean data
$stmt = $pdo->prepare("INSERT INTO contacts (name, email, message) VALUES (?, ?, ?)");
$stmt->execute([
$result['data']['name'],
$result['data']['email'],
$result['data']['message']
]);
echo "Form submitted successfully";
} else {
// Display errors
foreach ($result['errors'] as $field => $error) {
echo "$field: $error<br>";
}
}
}
?>- ✅ Always validate on the server
- ✅ Use PHP filter functions (FILTER_VALIDATE_*)
- ✅ Implement whitelist validation when possible
- ✅ Sanitize input before validation
- ✅ Set length limits on all string inputs
- ✅ Use regex patterns for complex validation
- ✅ Prevent directory traversal with basename()
🔐 Final Security Checklist
Before deploying any PHP project:
- ✔ Escape output with htmlspecialchars()
- ✔ Validate & sanitize all inputs
- ✔ Never trust $_GET / $_POST / $_COOKIE
- ✔ Use prepared statements (PDO/MySQLi)
- ✔ Never build SQL strings manually
- ✔ Use least privilege principle for DB users
- ✔ Regenerate session ID on login
- ✔ Use secure, httponly cookies
- ✔ Use password_hash() and password_verify()
- ✔ Implement CSRF tokens
- ✔ Restrict file types with MIME validation
- ✔ Rename uploads
- ✔ Store uploads outside web root
- ✔ Prevent directory traversal
- ✔ Disable dangerous PHP functions (exec, eval, etc.)
- ✔ Use HTTPS everywhere
- ✔ Limit error display in production
- ✔ Keep PHP updated (use 8.x)
- ✔ Set proper file permissions
Conclusion
PHP is powerful — but only if used safely.
Implement the practices in this article and you'll block 90%+ of common attacks, protect your users, and keep your projects secure.
Security isn't optional — it's essential. Master these techniques and build with confidence.
Related articles
- Working With Sessions and Cookies in PHP — How PHP stores user data, remembers login states, and powers modern web applications. Learn sessions, cookies, and secure authentication patterns.
- Building Your First REST API with PHP — Step-by-step guide to creating a RESTful API using PHP and best practices for API design.