PHP Sessions and Cookies
AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.
HTTP is stateless: each request stands alone. To “remember” a user you need cookies (stored in the browser) and sessions (stored on the server).
The division of labour
| Cookie | Session | |
|---|---|---|
| Stored in | Browser | Server (files by default) |
| Capacity | ~4 KB | Limited by the server, can be larger |
| Trust | User can read and edit it | User only holds an ID |
| Typical use | Preferences, non-sensitive flags | Login state, cart, permissions |
Sessions depend on cookies: the session ID travels in a cookie named PHPSESSID by default.
Using sessions
<?php
declare(strict_types=1);
session_start();
// write
$_SESSION["user_id"] = 42;
$_SESSION["cart"] = ["item_1" => 2];
// read
$userId = $_SESSION["user_id"] ?? null;
// remove one key
unset($_SESSION["cart"]);
session_start() must run before any output, otherwise you get “headers already sent”. The usual culprits are a BOM at the top of the file or a stray newline after a closing ?>.
Destroying a session properly
Logging out takes three steps, and skipping one leaves state behind:
session_start();
$_SESSION = []; // 1. clear the data
if (ini_get("session.use_cookies")) { // 2. delete the session cookie
$params = session_get_cookie_params();
setcookie(
session_name(),
"",
[
"expires" => time() - 42000,
"path" => $params["path"],
"domain" => $params["domain"],
"secure" => $params["secure"],
"httponly" => $params["httponly"],
"samesite" => $params["samesite"],
]
);
}
session_destroy(); // 3. remove server-side storage
Calling only session_destroy() leaves $_SESSION readable for the rest of the current request.
Session fixation and ID rotation
After a successful login you must regenerate the session ID, otherwise an attacker who planted a known ID can hijack the authenticated session:
session_start();
if (password_verify($password, $hash)) {
session_regenerate_id(true); // true = delete the old session file
$_SESSION["user_id"] = $user["id"];
$_SESSION["logged_in_at"] = time();
}
Setting cookies
PHP 7.3+ accepts an options array, which is far more readable than positional arguments:
setcookie("theme", "dark", [
"expires" => time() + 86400 * 30, // 30 days
"path" => "/",
"domain" => "", // current host
"secure" => true, // HTTPS only
"httponly" => true, // not readable from JavaScript
"samesite" => "Lax", // cross-site restrictions
]);
What the flags do:
secure— sent over HTTPS only. Set it tofalsewhile developing on plain HTTP, and back totruein production.httponly— hides the cookie fromdocument.cookie, limiting what XSS can steal.samesite—Lax(good default: cookies ride along normal navigation),Strict(never cross-site),None(cross-site allowed, requiressecure).
Delete a cookie by expiring it:
setcookie("theme", "", ["expires" => time() - 3600, "path" => "/"]);
Reading cookies
$theme = $_COOKIE["theme"] ?? "light";
// allow-list the value: users can set it to anything
$theme = in_array($theme, ["light", "dark"], true) ? $theme : "light";
Cookie contents are under the user’s control. Validate before use and never treat them as an identity source — do not decide who is logged in from a user ID stored in a cookie.
Storage and configuration
Sessions default to the server’s temporary directory. To move or change them:
session_save_path(__DIR__ . "/storage/sessions"); // before session_start()
session_start();
Useful php.ini settings:
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax
session.gc_maxlifetime = 1440 ; lifetime of session data, seconds
session.use_strict_mode = 1 ; accept only IDs this server created
Keep session.use_strict_mode = 1 on — it blocks an attacker from choosing a session ID.
Worked example: login and access control
<?php
declare(strict_types=1);
require __DIR__ . "/bootstrap.php"; // session configuration lives here
session_start();
function requireLogin(): void
{
if (empty($_SESSION["user_id"])) {
header("Location: /login.php", true, 303);
exit;
}
}
function login(int $userId): void
{
session_regenerate_id(true);
$_SESSION["user_id"] = $userId;
$_SESSION["logged_in_at"] = time();
}
function logout(): void
{
$_SESSION = [];
session_destroy();
}
requireLogin();
echo "Current user ID: " . (int) $_SESSION["user_id"];
Put the check on every protected entry point, not just the login page: users can request any URL directly.
Common mistakes
1. Output before session_start()
“Headers already sent”. Look for a BOM or a trailing newline after ?>.
2. Not rotating the ID after login
Session fixation risk. Call session_regenerate_id(true).
3. Only calling session_destroy() on logout
$_SESSION is still readable in that request. Clear it first, then delete the cookie.
4. Storing sensitive data in cookies
They are visible and editable. Keep sensitive values server-side.
5. Using a cookie value without validation
Users can forge it. Allow-list or cast it.
6. Shipping without secure / httponly
The cookie travels in clear text or is readable by any script on the page.
7. Checking permissions only at login
Every protected page must verify independently.
Summary
- Cookies live in the browser, are untrusted and small; sessions live on the server, keyed by an ID.
session_start()before any output.- On login
session_regenerate_id(true); on logout clear$_SESSION, expire the cookie, thensession_destroy(). - Always set
secure,httponlyandsamesitein production. - Authorise on each protected entry point, not once at login.
Next: the PHP basics series is complete — Composer, namespaces and autoloading come next in the intermediate series.