Codeskill

Learn to code, step by step

Security deep dive – CSRF, XSS, headers, rate limiting

Past “escape output and use prepared statements” – the baseline from intro and intermediate. CSRF, XSS, security headers, and rate limiting are the layers that stop small apps becoming easy targets.

Defence in depth

No single fix covers everything. Validate input, bind SQL parameters, escape on output, check intent on state-changing requests, and tell the browser to behave. Miss one layer and the others still help.

CSRF – prove the form came from you

Cross-Site Request Forgery tricks a logged-in user’s browser into POSTing to your site without their intent. Fix: a random token in the session, echoed in every form, checked on submit.

// bootstrap (once per session)
if (empty($_SESSION['csrf'])) {
    $_SESSION['csrf'] = bin2hex(random_bytes(32));
}

// in the form template
<input type="hidden" name="_token" value="<?= htmlspecialchars($_SESSION['csrf'], ENT_QUOTES, 'UTF-8') ?>">

// on POST
if (!hash_equals($_SESSION['csrf'], $_POST['_token'] ?? '')) {
    abort(419, 'CSRF token mismatch.');
}

Use hash_equals for timing-safe comparison. Rotate the token after login if you want belt-and-braces.

XSS – never trust strings in HTML

Stored XSS: attacker saves <script>... in a comment; your page renders it. Reflected XSS: malicious query string echoed back. Fix: escape on output with context in mind.

// HTML body text
echo htmlspecialchars($userInput, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

// inside a JavaScript string - different rules; prefer json_encode
echo json_encode($value, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);

Rich text is harder. If users need formatting, use a allowlist HTML sanitiser (HTML Purifier, etc.) – not strip_tags alone.

Security headers

Send these from PHP or your web server config. PHP example in a bootstrap file:

header('X-Frame-Options: SAMEORIGIN');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'");

Content-Security-Policy (CSP) is the heavy lifter against inline script injection. Tighten it gradually – a broken CSP breaks your own JS first.

Rate limiting

Login, password reset, and public forms attract bots. Limit attempts per IP or per account using a sliding window in Redis, Memcached, or a simple file cache:

final class RateLimiter
{
    public function tooManyAttempts(string $key, int $max, int $windowSeconds): bool
    {
        $path = sys_get_temp_dir() . '/rl_' . Hash('sha256', $key);
        $data = is_file($path) ? json_decode(file_get_contents($path), true) : ['count' => 0, 'reset' => time() + $windowSeconds];

        if (time() > $data['reset']) {
            $data = ['count' => 0, 'reset' => time() + $windowSeconds];
        }

        $data['count']++;
        file_put_contents($path, json_encode($data), LOCK_EX);

        return $data['count'] > $max;
    }
}

Return 429 Too Many Requests with a plain message. Log repeated hits – they often precede something worse.

Checklist before you ship

  • Prepared statements on every query with user input
  • CSRF on all state-changing forms and AJAX POSTs
  • password_hash / password_verify – never roll your own
  • Session cookie: HttpOnly, Secure on HTTPS, SameSite=Lax minimum
  • Uploads: check MIME and extension, store outside web root, serve via script or signed URL
  • Errors: log details, show generic message to users
PreviousMiddleware-style request pipelines