PHP Sessions - No session_regenerate_id Opens Hijack
Users saw others' order histories because session_regenerate_id was skipped.
20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Cookies store data in the browser — user can read and edit them.
- PHP sessions keep data on the server; browser holds only an opaque ID.
- session_start() and setcookie() must run before any HTML output.
- Call session_regenerate_id(true) after login to block session fixation.
- For "Remember Me", store a hashed token in DB, put raw token in a cookie.
- Sessions add disk/DB load per request; cookies cost the server nothing.
PHP Sessions - No session_regenerate_id Opens Hijack is a security vulnerability where a web application fails to call session_regenerate_id() after a user authenticates, allowing an attacker to perform session fixation or session hijacking. The session ID remains unchanged from the pre-authentication state, meaning if an attacker previously obtained or set the user's session ID (e.g., via a link or cross-site scripting), that same ID remains valid after login, granting the attacker access to the authenticated session.
Imagine you walk into a coffee shop. The barista hands you a numbered ticket (that's a cookie — it lives in your pocket). When you go back to the counter, you show the ticket and they look up your order in their notebook (that's the session — it lives on the server). The ticket is just a number; the real information is kept safely behind the counter. That's exactly how PHP sessions and cookies work together.
Every time you log into a website, add something to a shopping cart, or see your name in the top-right corner of a page, something has to remember who you are. HTTP — the protocol the web runs on — is completely stateless. Every request is a stranger walking in off the street. Without a mechanism to bridge those requests, your login would vanish the moment you clicked to the next page. That's not a quirk — it's a fundamental architectural problem that every web application must solve.
Sessions and cookies are the two tools PHP gives you to solve it. They work together, but they're not interchangeable. Get the choice wrong and you'll either leak sensitive data or struggle with performance. Here's the real difference: cookies hand the data to the browser, sessions keep it on your server. That one decision drives everything else.
Why Session Fixation Is Still a Threat
A PHP session is a server-side file that persists user state across HTTP requests, identified by a session ID stored in a cookie (default name: PHPSESSID). The core mechanic: the server reads the session ID from the incoming cookie, loads the corresponding session data from disk or cache, and makes it available via the $_SESSION superglobal. Without session_regenerate_id(), the same session ID is reused for the entire user lifetime, making it trivial for an attacker to hijack a session by obtaining a valid ID — often via a link with a pre-set PHPSESSID query parameter.
When a user logs in, the session ID should be regenerated to invalidate any previously shared or guessed ID. If you skip this step, the session ID remains constant from the first anonymous request through authenticated operations. An attacker who tricks a victim into clicking a link like example.com?PHPSESSID=known_id can then use that same ID after the victim logs in, gaining full access to the authenticated session. This is session fixation, and it's O(1) to exploit — no brute force needed.
Use session_regenerate_id(true) immediately after successful authentication — the true parameter deletes the old session file. This is not optional; it's a mandatory security control for any system handling user login. In production, failing to regenerate is the root cause of countless account takeover incidents, especially in legacy codebases or frameworks that don't enforce it by default.
session_regenerate_id() on login, and the server reused session files across requests due to a misconfigured session.save_path. Rule: always regenerate session ID on authentication and on any privilege change, and verify session storage isolation per tenant.Cookies — Storing Small Bits of Data in the User's Browser
A cookie is a tiny piece of text your server sends to the browser, which the browser then sends back on every subsequent request to that domain. Think of it as a sticky note you hand to your visitor and ask them to bring back every time they knock on your door.
Cookies are set with in PHP — and here's the critical detail that trips everyone up: you must call setcookie() before any HTML output reaches the browser, because cookies are sent as HTTP headers. Once the body starts streaming, headers are locked.setcookie()
Each cookie has a name, a value, and an expiry time. The expiry is a Unix timestamp — pass 0 and the cookie dies when the browser closes (a 'session cookie' in browser terminology, not to be confused with a PHP session). Pass and it survives for exactly one day.time() + 86400
Cookies are best for lightweight, non-sensitive preferences: theme choice, language, a 'remember me' token that points to server-side data. Never store a password, a credit card number, or a user ID in a raw cookie — the user can read and edit every cookie in their browser's dev tools.
<?php // ───────────────────────────────────────────────────────────── // cookie_preference.php // Demonstrates setting, reading, and deleting a cookie that // stores the user's preferred UI theme. // ───────────────────────────────────────────────────────────── $cookieName = 'user_theme'; // The key we'll look for in $_COOKIE $cookieValue = 'dark'; // The preference we're saving $cookieExpiry = time() + (86400 * 30); // 30 days from right now $cookiePath = '/'; // Available across the whole site $cookieDomain = ''; // Empty = current domain only $secureCookie = false; // Set TRUE in production (HTTPS only) $httpOnlyCookie = true; // JS cannot read this cookie — safer // setcookie() MUST come before any echo/HTML output setcookie( $cookieName, $cookieValue, [ 'expires' => $cookieExpiry, 'path' => $cookiePath, 'domain' => $cookieDomain, 'secure' => $secureCookie, 'httponly' => $httpOnlyCookie, 'samesite' => 'Lax' // Protects against CSRF cookie theft ] ); // ── Reading the cookie on the NEXT request ────────────────── // $_COOKIE is populated from the browser's cookie jar. // On THIS request the cookie isn't in $_COOKIE yet — // the browser only sends it back on the NEXT request. if (isset($_COOKIE[$cookieName])) { $savedTheme = htmlspecialchars($_COOKIE[$cookieName]); // Always sanitise! echo "Welcome back! Your saved theme is: " . $savedTheme . "\n"; } else { echo "No theme preference found — using default light theme.\n"; } // ── Deleting a cookie ─────────────────────────────────────── // You can't 'delete' a cookie directly — you overwrite it // with an expiry time in the past. setcookie($cookieName, '', [ 'expires' => time() - 3600, // 1 hour in the past = instant deletion 'path' => '/', 'httponly' => true, 'samesite' => 'Lax' ]); echo "Cookie scheduled for deletion.\n"; ?>
setcookie() call. The fix: move setcookie() to the very top of the file, and check for accidental whitespace or a BOM character in your file encoding.setcookie() to the top of the file, before any HTML, echo, or even whitespace.setcookie()+session_start() as strict as a database connection — do it first.setcookie() silently discards the cookie.setcookie() at the top of your script, before any output.PHP Sessions — Keeping Sensitive State on the Server
A PHP session stores data on the server and gives the browser a single, random session ID (by default stored in a cookie named PHPSESSID). The browser presents that ID on each request, and PHP uses it to look up the right data file on disk. The user sees only an opaque random string — not your actual data.
This is fundamentally more secure than cookies for anything sensitive, because the data never travels over the wire. An attacker who intercepts a session ID can hijack a session, but they can't read or forge the underlying data just from the ID alone.
Start a session with session_start() — again, before any output. Then read and write to the $_SESSION superglobal like a regular array. PHP handles serialisation, file locking, and garbage collection for you.
Sessions have a default lifetime tied to when the browser closes, but you can extend this by adjusting session.gc_maxlifetime in php.ini, or by updating a last-activity timestamp in $_SESSION yourself and expiring it manually — which gives you much more precise control than relying on the garbage collector.
<?php // ───────────────────────────────────────────────────────────── // session_login.php // A realistic login + session workflow. // In production you'd query a database; here we use a hardcoded // user to keep the focus on session mechanics. // ───────────────────────────────────────────────────────────── session_start(); // MUST be the first thing — before any output // ── Simulated user database ────────────────────────────────── $registeredUsers = [ 'alice@example.com' => [ 'password_hash' => password_hash('s3cureP@ss', PASSWORD_BCRYPT), 'display_name' => 'Alice Ng', 'role' => 'editor' ] ]; // ── Handle login form submission ───────────────────────────── if ($_SERVER['REQUEST_METHOD'] === 'POST') { $submittedEmail = trim($_POST['email'] ?? ''); $submittedPassword = $_POST['password'] ?? ''; if ( isset($registeredUsers[$submittedEmail]) && password_verify($submittedPassword, $registeredUsers[$submittedEmail]['password_hash']) ) { // Credentials are valid — regenerate session ID BEFORE writing data. // This prevents session fixation attacks. session_regenerate_id(true); // Store only what you need — not the whole user record $_SESSION['user_email'] = $submittedEmail; $_SESSION['user_display_name'] = $registeredUsers[$submittedEmail]['display_name']; $_SESSION['user_role'] = $registeredUsers[$submittedEmail]['role']; $_SESSION['logged_in_at'] = time(); // Track session age echo "Login successful. Hello, " . htmlspecialchars($_SESSION['user_display_name']) . "!\n"; } else { echo "Invalid email or password.\n"; } } // ── Checking if a user is already logged in ────────────────── function isUserLoggedIn(): bool { // Check the flag AND enforce a session timeout of 30 minutes $sessionTimeoutSeconds = 1800; if (!isset($_SESSION['user_email'], $_SESSION['logged_in_at'])) { return false; } if ((time() - $_SESSION['logged_in_at']) > $sessionTimeoutSeconds) { session_unset(); // Clear session data session_destroy(); // Delete the session file on disk return false; } // Refresh the activity timestamp so active users don't get booted $_SESSION['logged_in_at'] = time(); return true; } if (isUserLoggedIn()) { echo "Welcome back, " . htmlspecialchars($_SESSION['user_display_name']) . "!\n"; echo "Your role is: " . htmlspecialchars($_SESSION['user_role']) . "\n"; } // ── Logging out ────────────────────────────────────────────── function logoutUser(): void { session_start(); // Must start before destroying // Wipe all session variables first $_SESSION = []; // Expire the session cookie in the browser if (ini_get('session.use_cookies')) { $cookieParams = session_get_cookie_params(); setcookie( session_name(), // Usually 'PHPSESSID' '', time() - 42000, $cookieParams['path'], $cookieParams['domain'], $cookieParams['secure'], $cookieParams['httponly'] ); } session_destroy(); // Remove the server-side session file echo "You have been logged out.\n"; } ?>
ini_set().Sessions vs Cookies — Choosing the Right Tool for the Job
Now that you've seen both in action, let's talk about the decision you'll make constantly as a PHP developer: which one do I reach for?
The rule of thumb is deceptively simple: if it's sensitive or needs to be trustworthy, it goes in the session. If it's a low-stakes preference and you want it to outlive a browser restart, a cookie is fine.
Where it gets interesting is 'remember me' functionality. You don't actually store login state in a cookie. Instead, you generate a cryptographically random token, store it hashed in your database linked to the user, put the raw token in a long-lived cookie, and when that cookie is presented, you look up the hash, verify it, and silently start a new session. This way the cookie is useless to an attacker without the database.
Performance is another consideration. Sessions read from disk (or a cache layer like Redis in production) on every request. For very high-traffic applications, storing session data in Redis with session_set_save_handler() or a PHP session handler extension is standard practice. Cookies, being client-side, add zero server load — which is why JWTs have become popular for stateless APIs, though that's a topic for another day.
<?php // ───────────────────────────────────────────────────────────── // remember_me_token.php // Implements a secure 'Remember Me' flow. // This is the pattern used by Laravel, Symfony, and most // serious PHP frameworks under the hood. // ───────────────────────────────────────────────────────────── define('REMEMBER_ME_COOKIE', 'remember_token'); define('REMEMBER_ME_DAYS', 30); /** * Called at login when the user ticks 'Remember me'. * $userId — the authenticated user's database ID */ function issueRememberMeToken(int $userId, PDO $db): void { // Generate a cryptographically secure random token $rawToken = bin2hex(random_bytes(32)); // 64-char hex string $hashedToken = hash('sha256', $rawToken); // Never store raw tokens $expiresAt = date('Y-m-d H:i:s', time() + (86400 * REMEMBER_ME_DAYS)); // Persist the hashed token in the database $statement = $db->prepare( 'INSERT INTO remember_me_tokens (user_id, token_hash, expires_at) VALUES (:user_id, :token_hash, :expires_at)' ); $statement->execute([ ':user_id' => $userId, ':token_hash' => $hashedToken, ':expires_at' => $expiresAt ]); // Store only the RAW token in the cookie — the hash stays on the server setcookie(REMEMBER_ME_COOKIE, $rawToken, [ 'expires' => time() + (86400 * REMEMBER_ME_DAYS), 'path' => '/', 'secure' => true, // HTTPS only in production 'httponly' => true, // Not accessible via JavaScript 'samesite' => 'Lax' ]); echo "Remember-me token issued. Cookie will last " . REMEMBER_ME_DAYS . " days.\n"; } /** * Called on each page load to silently log in a returning user. * Returns the user_id if the token is valid, or null if not. */ function resolveRememberMeToken(PDO $db): ?int { if (!isset($_COOKIE[REMEMBER_ME_COOKIE])) { return null; // No cookie, nothing to do } $rawToken = $_COOKIE[REMEMBER_ME_COOKIE]; $hashedToken = hash('sha256', $rawToken); // Re-hash to look up in DB $statement = $db->prepare( 'SELECT user_id FROM remember_me_tokens WHERE token_hash = :token_hash AND expires_at > NOW()' ); $statement->execute([':token_hash' => $hashedToken]); $row = $statement->fetch(PDO::FETCH_ASSOC); if (!$row) { // Token not found or expired — clear the stale cookie setcookie(REMEMBER_ME_COOKIE, '', ['expires' => time() - 3600, 'path' => '/']); return null; } // Token is valid — start a fresh session for this user session_start(); session_regenerate_id(true); $_SESSION['user_id'] = (int) $row['user_id']; $_SESSION['logged_in_at'] = time(); echo "Silent login successful for user ID: " . $row['user_id'] . "\n"; return (int) $row['user_id']; } // ── Usage example (assuming $pdo is a connected PDO instance) ─ // issueRememberMeToken(42, $pdo); // $userId = resolveRememberMeToken($pdo); ?>
Session Security — Preventing Hijacking and Fixation
Sessions are secure by design — data stays on the server. But the session ID itself is a key that can be stolen or forged. Here are the three controls that matter in production:
- Regenerate the session ID on privilege changes — you already know this from the fixation warning. But also regenerate on role changes, password changes, and any escalation.
- Bind the session to the user's browser fingerprint — store a hash of the User-Agent and/or a subset of the IP address in the session. If the fingerprint changes mid-session, destroy the session and force re-login. This blocks session hijacking after ID theft.
- Set session cookie flags — HttpOnly (prevents JS access), Secure (only over HTTPS), SameSite (Lax or Strict to stop CSRF). These are not set by default in all PHP versions — you must configure them explicitly.
Here's a practical setup that hardens sessions for most applications:
<?php // ───────────────────────────────────────────────────────────── // session_hardening.php // Apply these settings early, before session_start() // ───────────────────────────────────────────────────────────── // Force cookies-only sessions (no URL propagation) ini_set('session.use_only_cookies', 1); // Prevent session ID from being passed via URL ini_set('session.use_trans_sid', 0); // Set session cookie parameters session_set_cookie_params([ 'lifetime' => 0, // Until browser closes 'path' => '/', 'domain' => '', 'secure' => true, // Only over HTTPS 'httponly' => true, // JS cannot access 'samesite' => 'Lax' // CSRF protection ]); // Optional: bind session to user agent if (isset($_SESSION['user_agent_hash'])) { $currentHash = md5($_SERVER['HTTP_USER_AGENT'] ?? ''); if ($_SESSION['user_agent_hash'] !== $currentHash) { session_destroy(); echo "Session hijacking detected. Redirecting to login.\n"; exit; } } else { $_SESSION['user_agent_hash'] = md5($_SERVER['HTTP_USER_AGENT'] ?? ''); } // Optional: set a custom entropy for better session IDs ini_set('session.entropy_file', '/dev/urandom'); ini_set('session.entropy_length', 32); session_start(); // Now safe to start ?>
- Regenerate the key after login — the old one doesn't work anymore.
- Bind the key to the guest's appearance (User-Agent) — if someone else tries it, slam the door.
- Only let the key work over secure channels (HTTPS) and don't let the bellhop (JavaScript) copy it.
Scaling Sessions — Beyond File-Based Storage
By default, PHP stores session data in files on the server's filesystem. This works fine for a single server, but falls apart as soon as you add a second web server behind a load balancer. User A's session data lives on Server 1; the next request hits Server 2, and PHP can't find the session file. The user gets logged out.
The industry standard solution is a shared session storage backend. Redis is the most popular choice for PHP. It's fast, in-memory, and supports automatic expiry. A Redis session handler uses or a PHP extension like session_set_save_handler()redis.
Here's a practical setup using the predis/predis library (or the native redis extension) to store sessions in Redis:
<?php // ───────────────────────────────────────────────────────────── // redis_session_handler.php // Configure PHP to use Redis for session storage. // ───────────────────────────────────────────────────────────── // Option 1: Use the native redis extension (recommended) // Install: pecl install redis ini_set('session.save_handler', 'redis'); ini_set('session.save_path', 'tcp://127.0.0.1:6379?prefix=PHPSESSIONS:'); // Option 2: If you can't install the extension, use predis via custom handler // (Simplified — in production, use a robust handler library) session_start(); // Now all session data is stored in Redis, shared across all servers $_SESSION['user_id'] = 42; echo "Session stored in Redis.\n"; ?> // ── php.ini equivalent ─────────────────────────────────────── // session.save_handler = redis // session.save_path = "tcp://127.0.0.1:6379?auth=mypassword&prefix=PHPSESSIONS:"
session_set_save_handler() with custom read/write/close/destroy/gc functions. This is slower than Redis but works across servers. Many frameworks like Laravel provide DB session drivers out of the box.What Is a Cookie — And Why Your App Can't Live Without It
HTTP is stateless. Every request is a stranger knocking on your server's door, and without cookies, you'd have no idea if they were the same user who just logged in two seconds ago. A cookie is a 4KB text file the server plants on the client machine. The browser sends it back with every subsequent request to your domain. That's how you know who's who.
Crucially, cookies are domain-locked. A cookie set by shop.example.com won't be sent to analytics.example.com — unless both are explicitly sharing via subdomain configuration. This is not just a privacy feature; it's a security boundary. Third-party cookies, the ones set by embedded ad scripts, bypass this boundary by design, which is why modern browsers are killing them off.
You use cookies for one thing: remembering the user's browser. Preferences, session tokens, A/B test buckets — stuff that doesn't need server-side secrecy. Never store sensitive data like passwords or credit card numbers in a cookie. It's a text file stored in a temp folder, not a vault.
// io.thecodeforge — php tutorial $user_id = 2047; $token = bin2hex(random_bytes(16)); $expires = time() + 86400 * 30; // 30 days setcookie( 'session_token', $token, $expires, '/', 'shop.example.com', true, // secure — HTTPS only true // httponly — JS can't touch it ); echo "Cookie set for user $user_id";
httponly flag lets any XSS vulnerability read your session token from document.cookie. Always set httponly=true for any cookie tied to authentication.Retrieving and Deleting Cookies — The Two Operations That Matter
Reading a cookie back is trivial: check the $_COOKIE superglobal. But here's the thing — $_COOKIE is populated at request start, before your script runs. If you change a cookie mid-request with , you won't see the new value in setcookie()$_COOKIE until the next page load. That's a rookie mistake that causes silent logic errors.
Deleting a cookie is even less intuitive. There's no . You delete a cookie by setting it with an expiration time in the past — typically one hour ago. The browser sees the expired timestamp and removes the file. Do this for every cookie you set, or it lingers forever on the client. Also, you must match the same path and domain you used when creating it, or the deletion silently fails.unsetcookie()
Here's the pattern you'll use in production. Note the consistent path and domain parameters between set and delete.
// io.thecodeforge — php tutorial // Retrieve the cookie securely if (isset($_COOKIE['session_token'])) { $stored_token = $_COOKIE['session_token']; echo "Found token: " . substr($stored_token, 0, 8) . "...\n"; } else { echo "No session token cookie found.\n"; } // Delete the cookie setcookie( 'session_token', '', time() - 3600, // expired 1 hour ago '/', 'shop.example.com', true, true ); echo "Cookie 'session_token' deleted.";
var_dump($_COOKIE) in production — it leaks to any logged-in user in error logs. Use a dedicated debug endpoint gated by IP instead.What Is a Session — And Why File-Based Storage Is a Debt Collector
Sessions are the server-side counterpart to cookies. Instead of shoving data into a 4KB client-side text file, you store a session ID in a cookie, and keep the actual data — cart items, user ID, CSRF tokens — on your server. This is non-negotiable for anything sensitive. If your session data hits the client, you've already lost.
By default, PHP stores sessions as files in /tmp/. On a single server, this works fine until it doesn't. The moment you scale to two web servers, you've got a classic problem: user authenticates on server A, but the next request hits server B, which has no idea who they are. That's when developers reach for shared storage like Redis or memcached. But before you do that, ask yourself: do you even need session data on the server? If you're just storing a user ID, a signed JWT in a cookie is simpler and faster.
Sessions have a clear lifecycle: start with , store data in session_start()$_SESSION, destroy with on logout. Forget to call session_destroy() on every page that needs session data, and session_start()$_SESSION is just an empty ghost.
// io.thecodeforge — php tutorial session_start(); // Store session data $_SESSION['user_id'] = 2047; $_SESSION['cart_total'] = 49.99; echo "Session started. User ID: " . $_SESSION['user_id'] . "\n"; // Later, on logout: session_unset(); // clear all vars session_destroy(); // kill session file on server setcookie('PHPSESSID', '', time() - 3600, '/'); echo "Session destroyed.";
session_destroy(), always explicitly delete the session cookie. Otherwise, the browser resends the old session ID and PHP may create a new session file with data from the void — a common source of ghost sessions.Secure Session Configuration for Production
In production environments, session security must be hardened beyond default PHP settings. The SameSite attribute prevents CSRF attacks by restricting cookie sending on cross-site requests. Setting SameSite to 'Lax' or 'Strict' ensures cookies are only sent for same-site requests. The HttpOnly flag prevents client-side scripts from accessing the cookie, mitigating XSS attacks. The Secure flag ensures cookies are only transmitted over HTTPS, preventing man-in-the-middle attacks. PHP ini directives like session.cookie_samesite, session.cookie_httponly, and session.cookie_secure should be set. Additionally, use session.cookie_lifetime = 0 to make session cookies non-persistent, and session.use_strict_mode = 1 to reject uninitialized session IDs. Example configuration:
<?php // Set secure session cookie parameters before session_start() ini_set('session.cookie_httponly', 1); ini_set('session.cookie_secure', 1); // Only if using HTTPS ini_set('session.cookie_samesite', 'Lax'); ini_set('session.use_strict_mode', 1); ini_set('session.use_only_cookies', 1); ini_set('session.cookie_lifetime', 0); // Session cookie (non-persistent) session_start(); ?> <!-- Or via php.ini --> session.cookie_httponly = 1 session.cookie_secure = 1 session.cookie_samesite = "Lax" session.use_strict_mode = 1 session.use_only_cookies = 1 session.cookie_lifetime = 0
Redis Session Storage in Modern PHP
File-based session storage is not scalable for distributed applications. Redis provides an in-memory, persistent, and fast session store suitable for high-traffic sites. PHP's built-in Redis session handler can be configured via ini directives or programmatically. Redis supports TTL (time-to-live) for automatic session expiry, atomic operations, and replication. To use Redis sessions, install the phpredis extension or use Predis library. Configure session.save_handler = redis and session.save_path with a Redis connection string. Example:
<?php // Set Redis as session handler via ini ini_set('session.save_handler', 'redis'); ini_set('session.save_path', 'tcp://127.0.0.1:6379?prefix=PHPSESSID:'); session_start(); // Or programmatically with custom handler class RedisSessionHandler implements \SessionHandlerInterface { private $redis; public function __construct($redis) { $this->redis = $redis; } public function open($savePath, $sessionName): bool { return true; } public function close(): bool { return true; } public function read($id): string { return $this->redis->get($id) ?: ''; } public function write($id, $data): bool { return $this->redis->setex($id, ini_get('session.gc_maxlifetime'), $data); } public function destroy($id): bool { return $this->redis->del($id) > 0; } public function gc($maxlifetime): int { // Redis handles TTL automatically return 0; } } $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $handler = new RedisSessionHandler($redis); session_set_save_handler($handler, true); session_start(); ?> <!-- composer.json for Predis --> { "require": { "predis/predis": "^2.0" } }
Stateless Token Authentication vs Session-Based Auth
Session-based authentication stores state on the server (e.g., PHP sessions), while stateless token authentication (e.g., JWT) encodes user data in a token sent by the client. Sessions are simpler for traditional server-rendered apps but require server-side storage and scaling. Tokens are stateless, enabling easy scaling and cross-domain use, but require careful handling of token expiration and revocation. PHP sessions are vulnerable to CSRF and fixation; tokens mitigate CSRF if stored in Authorization header. However, tokens can be stolen via XSS if stored in localStorage. Example JWT implementation:
<?php // Using firebase/php-jwt library require_once 'vendor/autoload.php'; use Firebase\JWT\JWT; use Firebase\JWT\Key; $key = 'your-secret-key'; $payload = [ 'sub' => 123, 'name' => 'John Doe', 'iat' => time(), 'exp' => time() + 3600 // 1 hour ]; // Generate token $token = JWT::encode($payload, $key, 'HS256'); echo $token; // Verify token $decoded = JWT::decode($token, new Key($key, 'HS256')); print_r($decoded); ?> <!-- composer.json --> { "require": { "firebase/php-jwt": "^6.0" } }
Skipped session_regenerate_id Opens the Door to Hijacking
session_regenerate_id() after login. An attacker could craft a URL containing a known session ID (e.g., ?PHPSESSID=attacker_known_id), trick a victim into clicking it before login, and then use that same ID after the victim authenticated. The session data became accessible to the attacker.session_regenerate_id(true) immediately after password verification, and deleted the old session file by passing true. Also enforced session.use_only_cookies = 1 to prevent URL-based session propagation.- Always call session_regenerate_id(true) after every privilege elevation (login, role change).
- Disable URL-based session transport: set session.use_only_cookies = 1.
- Treat any session ID received from the client as potentially hostile until the user proves identity.
session_start() or setcookie()session_start() to the top of every script that uses sessions. Use output buffering (ob_start()) as a temporary workaround.session_start() is called on both pages. Check if the session cookie (PHPSESSID) is being sent and received — use browser DevTools. Ensure the session save path is writable and consistent across all servers (if load balanced, use a shared Redis/DB handler).setcookie() is called before any output. Check the cookie expiry: time() + N seconds for future, or time() - N to delete. Verify the path and domain parameters match the current URL. Use 'secure' => true only if the page is served over HTTPS.echo session_id(); // Should be the same on consecutive requestsvar_dump($_COOKIE); // Confirm the browser is sending the session cookiesession_start() at the very top of every page, before any output. Ensure session.save_path is writable.grep -rn 'echo\|print\|?>' index.php // Check for stray outputCheck for whitespace before <?php in all included files.ob_start() at the very beginning as a temporary patch.php -i | grep session.gc_maxlifetime // Show current settingecho $_SESSION['last_activity'] ?? 'not set';time() - $_SESSION['last_activity'] > 1800) { session_destroy(); }| Feature / Aspect | PHP Sessions | Cookies |
|---|---|---|
| Where data lives | Server (disk or cache) | User's browser |
| Data size limit | Effectively unlimited | ~4KB per cookie |
| Security | High — user sees only an ID | Low — user can read and edit values |
| Survives browser close? | No (by default) | Yes (if expiry is set) |
| Works without JavaScript? | Yes | Yes |
| Adds server load? | Yes — disk/DB read per request | No — zero server cost to read |
| Best for | Login state, cart contents, sensitive flags | Theme, language, remember-me tokens |
| Accessible via JavaScript? | No (not directly) | Only if httponly is false |
| Controlled by server? | Yes | Partially — browser can reject or expire |
| GDPR / consent required? | Session cookies: often exempt | Persistent cookies: yes, consent needed |
| File | Command / Code | Purpose |
|---|---|---|
| cookie_preference.php | $cookieName = 'user_theme'; // The key we'll look for in $_COOKIE | Cookies |
| session_login.php | session_start(); // MUST be the first thing — before any output | PHP Sessions |
| remember_me_token.php | define('REMEMBER_ME_COOKIE', 'remember_token'); | Sessions vs Cookies |
| session_hardening.php | ini_set('session.use_only_cookies', 1); | Session Security |
| redis_session_handler.php | ini_set('session.save_handler', 'redis'); | Scaling Sessions |
| SetCookieExample.php | $user_id = 2047; | What Is a Cookie |
| DeleteCookie.php | if (isset($_COOKIE['session_token'])) { | Retrieving and Deleting Cookies |
| SessionLifecycle.php | session_start(); | What Is a Session |
| session_config.php | ini_set('session.cookie_httponly', 1); | Secure Session Configuration for Production |
| redis_session.php | ini_set('session.save_handler', 'redis'); | Redis Session Storage in Modern PHP |
| jwt_auth.php | require_once 'vendor/autoload.php'; | Stateless Token Authentication vs Session-Based Auth |
Key takeaways
setcookie() and session_start() send HTTP headersInterview Questions on This Topic
What is the difference between a session and a cookie in PHP, and how do they work together under the hood?
What is a session fixation attack, and what single line of PHP code is the primary defence against it?
session_regenerate_id(true) called immediately after successful login. The true parameter deletes the old session file.If a user ticks 'Remember Me' on your login form, how would you implement that securely — and why is storing the user's ID in a cookie the wrong approach?
bin2hex(random_bytes(32))), store its SHA-256 hash in a database linked to the user ID with an expiry date, and set the raw token in a long-lived cookie (httponly, secure, samesite=Lax). On subsequent requests, read the cookie, hash it, look up the hash in the DB, and if valid, silently start a new session. This way, even if the cookie is stolen, the attacker cannot reverse the hash to obtain the original token, and token rotation invalidates old cookies.Frequently Asked Questions
By default, a PHP session lasts until the browser is closed, because the PHPSESSID cookie has no expiry set. On the server side, the session data file is eligible for garbage collection after session.gc_maxlifetime seconds (default 1440 — 24 minutes of inactivity). You can extend this in php.ini or by implementing your own timeout logic using a timestamp stored in $_SESSION.
Yes — PHP can pass the session ID in the URL as a query parameter (e.g. page.php?PHPSESSID=abc123) if you set session.use_trans_sid = 1 in php.ini. However, this is a serious security risk because session IDs appear in browser history, server logs, and Referer headers. Stick with cookie-based sessions and set session.use_only_cookies = 1 to enforce it.
session_unset() clears all variables stored in $_SESSION for the current session, but the session itself (and its server-side file) still exists. session_destroy() deletes the session file on disk but does NOT clear the $_SESSION superglobal in the current request. For a proper logout, you should do both: set $_SESSION = [] to clear variables, then call session_destroy() to remove the file, and finally expire the PHPSESSID cookie in the browser.
You cannot use the default file-based session handler across multiple servers. Instead, configure a shared session storage backend like Redis, Memcached, or a database. In php.ini set session.save_handler = redis and session.save_path = 'tcp://redis-host:6379'. Alternatively, implement a custom handler using that stores session data in a shared MySQL/MariaDB table.session_set_save_handler()
The SameSite attribute (Strict, Lax, or None) controls whether a cookie is sent with cross-site requests. Setting SameSite=Lax prevents the browser from sending the session cookie with POST requests from other sites, which is a strong defence against CSRF attacks. SameSite=Strict offers even more protection but may break some legitimate cross-site navigation. Use Lax for session cookies.
20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.
That's PHP Basics. Mark it forged?
7 min read · try the examples if you haven't