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:

Hackers know this — so they specifically target badly written PHP code.

Common consequences of poor security:

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:

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");
    }
}
?>

This blocks 99% of CSRF attacks.

4. Password Security

<?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;
}
?>

5. File Upload Vulnerabilities

Allowing users to upload files is extremely dangerous.

<?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:

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;
}
?>

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>";
        }
    }
}
?>

🔐 Final Security Checklist

Before deploying any PHP project:

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

Links on this page