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.
- 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.
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.
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.
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.
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:
- 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:
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.
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.
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.
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.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.Key takeaways
setcookie() and session_start() send HTTP headersCommon mistakes to avoid
4 patternsCalling session_start() or setcookie() after output has begun
session_start() and all setcookie() calls to the absolute top of every PHP file, before any echo, HTML, or even a blank line outside the <?php tag. Use output buffering (ob_start()) as a last resort in legacy code.Skipping session_regenerate_id(true) after login
session_regenerate_id(true) immediately after verifying login credentials. The 'true' argument deletes the old session file; without it, the old ID remains valid.Storing sensitive data directly in cookies
htmlspecialchars() before rendering any cookie value into HTML to prevent XSS.Using file sessions in a load-balanced environment
session_set_save_handler() or configure the native redis extension.Interview 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?
Frequently Asked Questions
20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.
That's PHP Basics. Mark it forged?
8 min read · try the examples if you haven't