PHP Fundamentals

Security Best Practices

15 min Lesson 44 of 45

Security Best Practices in PHP

Security is paramount in web development. A single vulnerability can compromise your entire application and user data. In this lesson, we'll cover essential security practices every PHP developer must follow.

1. Input Validation and Sanitization

Never trust user input. Always validate and sanitize data from forms, URLs, and cookies.

Golden Rule: Validate input, escape output, and never trust data from external sources.

Input Validation

<?php
// Validate email
$email = $_POST['email'] ?? '';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    die('Invalid email address');
}

// Validate integer
$id = $_GET['id'] ?? 0;
if (!filter_var($id, FILTER_VALIDATE_INT) || $id < 1) {
    die('Invalid ID');
}

// Validate URL
$website = $_POST['website'] ?? '';
if (!filter_var($website, FILTER_VALIDATE_URL)) {
    die('Invalid URL');
}

// Whitelist validation for specific values
$action = $_GET['action'] ?? '';
$allowed_actions = ['view', 'edit', 'delete'];
if (!in_array($action, $allowed_actions)) {
    die('Invalid action');
}

// Validate length
$username = $_POST['username'] ?? '';
if (strlen($username) < 3 || strlen($username) > 20) {
    die('Username must be 3-20 characters');
}

// Regex validation for alphanumeric
if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
    die('Username can only contain letters, numbers, and underscores');
}
?>

Input Sanitization

<?php
// Sanitize email
$email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);

// Sanitize string (remove tags)
$name = filter_var($_POST['name'], FILTER_SANITIZE_STRING);

// Remove HTML tags
$comment = strip_tags($_POST['comment']);

// Remove specific tags but keep others
$content = strip_tags($_POST['content'], '<p><br><strong><em>');

// Trim whitespace
$input = trim($_POST['input']);

// Remove special characters
$clean = preg_replace('/[^a-zA-Z0-9]/', '', $input);
?>

2. SQL Injection Prevention

SQL injection is one of the most dangerous vulnerabilities. Always use prepared statements.

Never Do This: Never concatenate user input directly into SQL queries!
<?php
// ❌ VULNERABLE - Never do this!
$username = $_POST['username'];
$query = "SELECT * FROM users WHERE username = '$username'";
// Attacker can input: ' OR '1'='1

// ✅ SAFE - Use prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);

// ✅ SAFE - Named parameters
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND email = :email");
$stmt->execute([
    'username' => $username,
    'email' => $email
]);

// ✅ SAFE - MySQLi prepared statements
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result();
?>

3. Cross-Site Scripting (XSS) Prevention

XSS attacks inject malicious JavaScript into your pages. Always escape output.

<?php
// Get user input
$comment = $_POST['comment'];

// Store in database (with prepared statements)
$stmt = $pdo->prepare("INSERT INTO comments (text) VALUES (?)");
$stmt->execute([$comment]);

// When displaying, ALWAYS escape
$comment = $row['text'];
?>

<!-- Display escaped output -->
<div class="comment">
    <?php echo htmlspecialchars($comment, ENT_QUOTES, 'UTF-8'); ?>
</div>

<?php
// Helper function for escaping
function e($string) {
    return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
?>

<div><?php echo e($user_input); ?></div>
Pro Tip: Modern templating engines like Twig and Blade automatically escape output by default.

4. Cross-Site Request Forgery (CSRF) Prevention

CSRF attacks trick users into performing unwanted actions. Use CSRF tokens for all state-changing operations.

<?php
// Generate CSRF token
session_start();
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// Include in forms
?>
<form method="POST" action="delete.php">
    <input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
    <button type="submit">Delete Account</button>
</form>

<?php
// Verify CSRF token on form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? '';

    if (!hash_equals($_SESSION['csrf_token'], $token)) {
        die('CSRF token validation failed');
    }

    // Process form...
}

// CSRF protection function
function verify_csrf_token($token) {
    return isset($_SESSION['csrf_token']) &&
           hash_equals($_SESSION['csrf_token'], $token);
}
?>

5. Password Security

Never store passwords in plain text. Use PHP's built-in password hashing functions.

<?php
// Hash password on registration
$password = $_POST['password'];
$hashed = password_hash($password, PASSWORD_DEFAULT);

// Store $hashed in database
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->execute([$username, $hashed]);

// Verify password on login
$stmt = $pdo->prepare("SELECT password FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();

if ($user && password_verify($_POST['password'], $user['password'])) {
    // Password correct - log user in
    $_SESSION['user_id'] = $user['id'];
    header('Location: dashboard.php');
} else {
    // Invalid credentials
    echo 'Invalid username or password';
}

// Check if password needs rehashing (algorithm updated)
if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
    $new_hash = password_hash($password, PASSWORD_DEFAULT);
    // Update database with new hash
}
?>
Password Requirements: Enforce minimum length (8+ characters), complexity, and consider using password strength meters.

6. Session Security

Secure session handling prevents session hijacking and fixation attacks.

<?php
// Secure session configuration
ini_set('session.cookie_httponly', 1);  // Prevent JavaScript access
ini_set('session.cookie_secure', 1);    // HTTPS only
ini_set('session.use_strict_mode', 1);  // Reject uninitialized session IDs
ini_set('session.cookie_samesite', 'Strict');  // CSRF protection

session_start();

// Regenerate session ID on login
function login_user($user_id) {
    session_regenerate_id(true);  // Prevent session fixation
    $_SESSION['user_id'] = $user_id;
    $_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
    $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
}

// Validate session
function validate_session() {
    if (!isset($_SESSION['user_id'])) {
        return false;
    }

    // Check IP address (optional - can cause issues with mobile users)
    if ($_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
        session_destroy();
        return false;
    }

    // Check user agent
    if ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
        session_destroy();
        return false;
    }

    return true;
}

// Logout
function logout() {
    $_SESSION = [];
    session_destroy();
    setcookie(session_name(), '', time() - 3600, '/');
}
?>

7. File Upload Security

File uploads are a common attack vector. Always validate and sanitize uploaded files.

<?php
// Secure file upload handling
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
    $file = $_FILES['avatar'];

    // Check for upload errors
    if ($file['error'] !== UPLOAD_ERR_OK) {
        die('Upload error');
    }

    // Validate file size (5MB max)
    $max_size = 5 * 1024 * 1024;
    if ($file['size'] > $max_size) {
        die('File too large');
    }

    // Validate MIME type
    $allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime_type = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);

    if (!in_array($mime_type, $allowed_types)) {
        die('Invalid file type');
    }

    // Validate file extension
    $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif'];
    $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));

    if (!in_array($extension, $allowed_extensions)) {
        die('Invalid file extension');
    }

    // Generate unique filename
    $new_filename = bin2hex(random_bytes(16)) . '.' . $extension;
    $upload_dir = 'uploads/';
    $destination = $upload_dir . $new_filename;

    // Move file outside web root if possible
    if (move_uploaded_file($file['tmp_name'], $destination)) {
        // Store filename in database
        echo 'File uploaded successfully';
    }
}
?>
Critical: Never trust the original filename or file extension. Always validate the actual file content.

8. Directory Traversal Prevention

Prevent attackers from accessing files outside the intended directory.

<?php
// ❌ VULNERABLE
$file = $_GET['file'];
include("pages/" . $file);
// Attacker can use: ../../etc/passwd

// ✅ SAFE - Whitelist approach
$allowed_files = ['home', 'about', 'contact'];
$file = $_GET['page'] ?? 'home';

if (in_array($file, $allowed_files)) {
    include("pages/{$file}.php");
} else {
    include("pages/404.php");
}

// ✅ SAFE - Sanitize path
function safe_include($filename) {
    // Remove directory traversal sequences
    $filename = str_replace(['../', '..\\'], '', $filename);

    // Build full path
    $base_dir = __DIR__ . '/pages/';
    $full_path = realpath($base_dir . $filename . '.php');

    // Verify file is within base directory
    if ($full_path && strpos($full_path, $base_dir) === 0 && file_exists($full_path)) {
        include($full_path);
    } else {
        include($base_dir . '404.php');
    }
}
?>

9. Error Handling and Logging

Don't expose sensitive information in error messages.

<?php
// Production settings in php.ini or .htaccess
ini_set('display_errors', 0);  // Don't show errors to users
ini_set('log_errors', 1);       // Log errors to file
ini_set('error_log', '/path/to/php-errors.log');

// Custom error handler
set_error_handler(function($errno, $errstr, $errfile, $errline) {
    // Log error
    error_log("Error [$errno]: $errstr in $errfile on line $errline");

    // Show generic message to user
    if (ini_get('display_errors')) {
        echo "<b>Error:</b> $errstr";
    } else {
        echo "An error occurred. Please try again later.";
    }
});

// Custom exception handler
set_exception_handler(function($exception) {
    error_log($exception->getMessage());

    if (!ini_get('display_errors')) {
        die('An unexpected error occurred.');
    }
});

// Try-catch for database errors
try {
    $pdo = new PDO($dsn, $user, $pass);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    error_log("Database error: " . $e->getMessage());
    die('Database connection failed');  // Don't reveal details
}
?>

10. Security Headers

Set HTTP security headers to protect against common attacks.

<?php
// Set security headers
header("X-Frame-Options: DENY");  // Prevent clickjacking
header("X-Content-Type-Options: nosniff");  // Prevent MIME sniffing
header("X-XSS-Protection: 1; mode=block");  // XSS protection
header("Referrer-Policy: strict-origin-when-cross-origin");
header("Content-Security-Policy: default-src 'self'");  // CSP

// For HTTPS sites
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
    header("Strict-Transport-Security: max-age=31536000; includeSubDomains");
}
?>

Security Checklist Exercise

Review a PHP application and verify:

  • All user input is validated and sanitized
  • All database queries use prepared statements
  • All output is properly escaped
  • CSRF tokens protect all forms
  • Passwords are hashed with password_hash()
  • Sessions are configured securely
  • File uploads are validated thoroughly
  • Error messages don't reveal sensitive information
  • Security headers are set
  • HTTPS is enforced

Additional Security Resources

  • OWASP Top 10: Study the most critical web application security risks
  • PHP Security Cheat Sheet: Quick reference for secure coding
  • Security Testing: Use tools like RIPS, PHPStan for static analysis
  • Dependency Scanning: Use Composer audit to check for vulnerable packages
  • Penetration Testing: Consider professional security audits for production apps
Remember: Security is not a one-time task. Stay updated with security patches, regularly review code, and follow security best practices throughout development.