PHP Sessions - No session_regenerate_id Opens Hijack
Users saw others' order histories because session_regenerate_id was skipped.
- 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.
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.
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 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.
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:
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:
| 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 |
Key Takeaways
- Cookies live in the browser — the user owns them and can edit them. Never store anything that grants access or trust in a raw cookie value.
- PHP sessions store data on the server; the browser only holds an opaque session ID. Call session_regenerate_id(true) immediately after login to block session fixation.
- Both
setcookie()andsession_start()send HTTP headers — they must be called before a single byte of output, or you'll get the 'headers already sent' error every time. - A 'remember me' feature is not a session and not a login cookie — it's a server-side token lookup: generate a random token, hash it in the DB, put the raw token in a long-lived cookie, and verify the hash on each visit.
- In a load-balanced environment, file-based sessions break. Switch to a shared Redis or database session handler.
Common Mistakes to Avoid
- Calling session_start() or setcookie() after output has begun
Symptom: "Warning: Cannot modify header information — headers already sent by (output started at index.php:1)". The session or cookie is silently not set.
Fix: Movesession_start()and allsetcookie()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
Symptom: No visible error, but the application is silently vulnerable to session fixation attacks where an attacker pre-sets a session ID and inherits the authenticated session.
Fix: Always callsession_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
Symptom: User opens browser DevTools > Application > Cookies and sees their user_id, role, or email in plain text, which they can edit to escalate privileges.
Fix: Store only opaque, meaningless tokens in cookies. Keep real data in $_SESSION or the database, and always callhtmlspecialchars()before rendering any cookie value into HTML to prevent XSS. - Using file sessions in a load-balanced environment
Symptom: Users repeatedly logged out or their cart appearing empty on different requests.
Fix: Switch to a shared session handler: Redis, Memcached, or a database. Useor configure the native redis extension.session_set_save_handler()
Interview Questions on This Topic
- QWhat is the difference between a session and a cookie in PHP, and how do they work together under the hood?JuniorReveal
- QWhat is a session fixation attack, and what single line of PHP code is the primary defence against it?Mid-levelReveal
- QIf 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?SeniorReveal
Frequently Asked Questions
How long does a PHP session last by default?
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.
Can I use PHP sessions without cookies?
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.
What is the difference between session_unset() and session_destroy()?
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.
How do I make sessions work across multiple servers behind a load balancer?
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()
What is SameSite cookie attribute and why should I use it?
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.
That's PHP Basics. Mark it forged?
4 min read · try the examples if you haven't