PHPSession Security

Session Security

A session id is effectively a password that gets sent on every request instead of typed once. Whoever holds a valid session id can act as the user it belongs to, without ever knowing their real password. Session security is entirely about making that id hard to steal, hard to guess, and hard to reuse once it should no longer be valid. This page covers the two attacks every PHP developer should understand — fixation and hijacking — and the concrete settings and function calls that defend against them.

Session fixation

In a fixation attack, the attacker does not steal an id — they hand the victim one. If an attacker can get a victim to visit the site using a session id the attacker already knows (for example through a crafted link, or because the id was accepted from a URL parameter), and the victim then logs in under that same id, the attacker's browser is now logged in too, because it holds the identical id. The defense is simple: never keep using the session id that existed before login. Generate a brand-new one the moment a visitor's privilege level changes.

Session hijacking

Hijacking is the more familiar attack: the attacker obtains a valid, already-authenticated session id — sniffed off an insecure network, read from an XSS-injected script, or leaked through a referrer header — and replays it to impersonate the victim. Unlike fixation, hijacking happens after login, so it cannot be solved by regenerating the id at login time alone. It needs the transport itself (HTTPS), the cookie flags, and some detection of "this looks like a different client than before" working together.

Regenerate the session id after login

session_regenerate_id(true) issues a new session id, copies the existing $_SESSION data over to it, and — because of the true argument — deletes the old session's data on the server so the previous id can no longer be used at all. This single call is the primary defense against fixation and should run immediately after any point where a visitor gains new privileges: logging in, switching accounts, or being promoted to an admin role.

Regenerating the session id after a successful login

PHP
<?php
session_start();

// ... verify username + password_verify() succeeded ...

session_regenerate_id(true); // new id, old session data destroyed server-side
$_SESSION['user_id'] = $user['id'];
$_SESSION['logged_in_at'] = time();
Without the true argument, the old session id still works
`session_regenerate_id()` with no argument (or `false`) generates a new id but leaves the old session data in place on the server for a short window, so a fixated id could still be replayed. Always pass `true` after a login so the old data is destroyed immediately.
Locking down the session cookie

The session id is only as safe as the cookie carrying it. Three php.ini directives (or their runtime equivalents via session_set_cookie_params()) control how that cookie behaves.

Setting

Effect

session.cookie_httponly

When on, JavaScript cannot read the session cookie via document.cookie, which stops most XSS-based theft of the id.

session.cookie_secure

When on, the browser only ever sends the session cookie over HTTPS, so it cannot leak in cleartext on an insecure network.

session.cookie_samesite

Set to Lax or Strict to stop the cookie from being sent on cross-site requests, reducing CSRF exposure that rides on the session.

Setting secure cookie params before session_start()

PHP
<?php
session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'domain' => '',
    'secure' => true,     // only over HTTPS
    'httponly' => true,   // not readable from JavaScript
    'samesite' => 'Lax',  // not sent on most cross-site requests
]);

session_start();
These settings belong in php.ini for a whole application
Calling `session_set_cookie_params()` per script works, but it is easy to forget on one entry point. Setting `session.cookie_httponly = 1`, `session.cookie_secure = 1`, and `session.cookie_samesite = Lax` in `php.ini` (or an `.htaccess`/`ini_set()` at the top of a shared bootstrap file) makes the secure behavior the default for every session in the application, rather than something each script has to remember.
Binding a session to IP and User-Agent

An extra layer some applications add is recording the visitor's IP address and User-Agent header alongside the session data at login, then comparing them on every later request. If either value changes mid-session, the application can treat it as suspicious and force a re-login. This does raise the bar for an attacker who has only stolen the session id itself, since they would also need to spoof a matching IP and User-Agent.

A simple (imperfect) binding check

PHP
<?php
session_start();

if (!isset($_SESSION['ip']) || !isset($_SESSION['ua'])) {
    $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
    $_SESSION['ua'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
} elseif (
    $_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']
    || $_SESSION['ua'] !== ($_SERVER['HTTP_USER_AGENT'] ?? '')
) {
    session_unset();
    session_destroy();
    // force the visitor to log in again
}
IP binding produces real false positives
Mobile users switch between Wi-Fi and cellular mid-session, and large organizations and mobile carriers route many users through a shared pool of outbound IP addresses (NAT, corporate proxies), so their visible IP can legitimately change between requests. Treating an IP change as an automatic hard logout will lock out real users. Many applications only warn or require re-authentication for sensitive actions instead of destroying the session outright, and some skip IP binding entirely for this reason. User-Agent checks are gentler but not bulletproof either — the header is client-supplied and can be spoofed by an attacker who already has the session id.
  • session_regenerate_id(true) immediately after login (and after any other privilege change) defeats fixation.

  • Set httponly, secure, and samesite on the session cookie so it cannot be read by scripts, sent in the clear, or replayed cross-site.

  • Serve the whole site over HTTPS — cookie_secure is meaningless without it.

  • Treat IP/User-Agent binding as a tripwire, not a guarantee, and expect some false positives.

  • Always give logged-in users a real way to log out that both destroys the server-side session and clears the cookie.

Tip
Keep session lifetimes short for sensitive applications (banking, admin panels) and pair a short `session.gc_maxlifetime` with an explicit "you have been logged out due to inactivity" message, rather than leaving a stale but still-valid session sitting around as a longer-lived target.