Sessions, passwords and web security

PHP · Advanced · 16 min

Authentication bugs are the costliest bugs in web apps, and PHP gives you the right primitives if you use them properly.

Sessions keep state across requests. session_start() sends a session id cookie and loads $_SESSION from storage (files by default, Redis in larger setups). Call it before any output. Configure the cookie safely: httponly (no JavaScript access), secure (HTTPS only) and samesite=Lax or Strict, via session_set_cookie_params() or php.ini.

After login, call session_regenerate_id(true). Otherwise an attacker who planted a known session id before login (session fixation) shares the authenticated session. On logout clear $_SESSION and call session_destroy().

Passwords: never store them or hash them with md5/sha1. Use password_hash($pw, PASSWORD_DEFAULT) (bcrypt today; PHP 8.4 raised its default cost to 12) and check with password_verify(). When the algorithm or cost changes, password_needs_rehash() tells you to rehash at the next login. Argon2id is available with PASSWORD_ARGON2ID.

CSRF: a malicious site can make the browser submit a form to yours with the user's cookies. Put a random token, bin2hex(random_bytes(32)), in the session and in every form, then compare with hash_equals(), which is constant-time.

The rest of the checklist: prepared statements against SQL injection, htmlspecialchars on output against XSS, random_bytes and random_int for anything secret (never rand), and never unserialize() untrusted data.

Example

<?php
session_set_cookie_params(['httponly' => true, 'secure' => true, 'samesite' => 'Lax']);
session_start();

$hash = password_hash('correct horse', PASSWORD_DEFAULT);
echo substr($hash, 0, 7), "\n";                  // $2y$12$ (bcrypt, cost 12 in 8.4)
var_dump(password_verify('correct horse', $hash)); // bool(true)
var_dump(password_verify('wrong', $hash));         // bool(false)

function login(int $userId): void {
    session_regenerate_id(true);       // defeat session fixation
    $_SESSION['user_id'] = $userId;
}

$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
// In the form: <input type="hidden" name="csrf" value="...">
function checkCsrf(string $sent): bool {
    return hash_equals($_SESSION['csrf'], $sent);
}
var_dump(checkCsrf('guess'));          // bool(false)

Remember

  • Regenerate the session id on login and set httponly, secure and samesite cookies
  • password_hash and password_verify are the only password API you need
  • Protect state-changing forms with a CSRF token compared via hash_equals

Watch out for

  • Comparing tokens or hashes with ==, which is not constant-time and is subject to type juggling.

Check yourself

  1. Why call session_regenerate_id(true) right after a successful login?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.php.net/manual/en/function.password-hash.php

Sessions, passwords and web security · PHP · Topolearn