PHP
04 / 06

Security: XSS, CSRF, Injection & Auth

PHP Security: XSS, CSRF, Injection & Auth

PHP applications are high-value targets because so much of the web runs PHP. Most vulnerabilities fall into a handful of categories — understanding each one and its mitigation is essential for any PHP developer.

XSS — Cross-Site Scripting

<?php
// XSS: attacker injects <script> that runs in victim's browser

// NEVER do this:
echo $_GET['name'];              // raw user input in HTML
echo "<b>" . $_POST['msg'] . "</b>";

// ALWAYS escape output with the correct context function:
// HTML context — htmlspecialchars (converts < > " ' & to entities)
echo htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

// Helper function
function e(string $value): string {
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
echo "<p>Hello, " . e($_GET['name']) . "</p>";

// HTML attribute context
echo '<input value="' . e($value) . '">';

// URL context
echo '<a href="' . e(urlencode($path)) . '">Link</a>';

// JavaScript context — use json_encode
echo '<script>var data = ' . json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP) . ';</script>';

// Content Security Policy header (defense-in-depth)
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'");

CSRF — Cross-Site Request Forgery

<?php
// CSRF: malicious site tricks authenticated user into submitting a form to your app

// 1. Generate token (store in session)
session_start();
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// 2. Embed in every state-changing form
?>
<form method="POST" action="/transfer">
    <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
    <input type="text" name="amount">
    <button type="submit">Transfer</button>
</form>
<?php

// 3. Validate on every POST/PUT/DELETE
function validateCsrfToken(): void {
    session_start();
    $token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
    if (!hash_equals($_SESSION['csrf_token'] ?? '', $token)) {
        http_response_code(403);
        die('Invalid CSRF token');
    }
}

// SameSite cookie attribute (additional mitigation)
session_set_cookie_params([
    'samesite' => 'Lax',   // or 'Strict'
    'secure' => true,
    'httponly' => true,
]);

SQL Injection & File Upload

<?php
// SQL Injection — always use prepared statements (see PDO section)
// Bad:
$id = $_GET['id'];
$result = $pdo->query("SELECT * FROM users WHERE id = $id");  // NEVER

// Good: prepared statement with bound parameter (see page 2)

// Command injection — never pass user input to shell
// Bad:
system("convert " . $_POST['filename'] . " output.jpg");  // NEVER

// Good: validate strictly, use escapeshellarg if unavoidable
$filename = basename($_POST['filename']);
if (!preg_match('/^[a-zA-Z0-9_.-]+$/', $filename)) {
    die('Invalid filename');
}
system("convert " . escapeshellarg("/uploads/" . $filename) . " output.jpg");

// File upload security
function handleUpload(array $file): string {
    // 1. Check for upload errors
    if ($file['error'] !== UPLOAD_ERR_OK) {
        throw new \RuntimeException('Upload error');
    }
    // 2. Validate real MIME type (not Content-Type header — easily spoofed)
    $finfo = new \finfo(FILEINFO_MIME_TYPE);
    $mimeType = $finfo->file($file['tmp_name']);
    $allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
    if (!in_array($mimeType, $allowed, true)) {
        throw new \RuntimeException('Invalid file type');
    }
    // 3. Validate size
    if ($file['size'] > 5 * 1024 * 1024) {
        throw new \RuntimeException('File too large');
    }
    // 4. Generate safe filename (never trust original name)
    $ext = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'][$mimeType];
    $newName = bin2hex(random_bytes(16)) . '.' . $ext;
    // 5. Store outside webroot or use randomized path
    $dest = '/var/app/uploads/' . $newName;
    move_uploaded_file($file['tmp_name'], $dest);
    return $newName;
}

Password Hashing & Session Security

<?php
// Password hashing — bcrypt (cost 12+ recommended)
$hash = password_hash($plaintext, PASSWORD_BCRYPT, ['cost' => 12]);
// or: PASSWORD_ARGON2ID (more secure, requires libsodium)
$hash = password_hash($plaintext, PASSWORD_ARGON2ID);

// Verify
if (password_verify($plaintext, $hash)) {
    // authenticated

    // Rehash if algorithm/cost changed
    if (password_needs_rehash($hash, PASSWORD_ARGON2ID)) {
        $newHash = password_hash($plaintext, PASSWORD_ARGON2ID);
        // save $newHash to DB
    }
}

// Cryptographically secure random values
$token = bin2hex(random_bytes(32));    // 64-char hex token
$otp = random_int(100000, 999999);     // 6-digit OTP

// Constant-time comparison (prevents timing attacks)
hash_equals($expectedToken, $submittedToken);  // never use ===

// Secure session configuration (php.ini or ini_set)
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.use_strict_mode', 1);   // reject uninitialized session IDs
ini_set('session.gc_maxlifetime', 3600);

session_start();
// Regenerate ID after privilege change (login, sudo)
session_regenerate_id(delete_old_session: true);

Security Headers & Tips

  • Set security headers: Content-Security-Policy, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy.

  • Keep PHP version current — EOL versions receive no security patches. Check php.net/supported-versions.

  • Disable dangerous php.ini settings: expose_php=Off, display_errors=Off (in production), allow_url_fopen=Off.

  • Use HTTPS everywhere. Redirect HTTP → HTTPS at the web server level, not PHP.

  • Rate-limit authentication endpoints to prevent brute-force. Log failed attempts.

  • Store secrets in environment variables (never in code or DB). Use phpdotenv in development.

  • Use a security scanning tool: Psalm taint analysis, RIPS, or SonarQube for static analysis.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free