SameSite, HttpOnly and the __Host- Prefix: Cookies Done Properly
Cookie security is a checklist that fits in one function call, and most tutorials still teach the positional-arguments version from 2003 that can't even express half the list. The modern signature (PHP 7.3+):
setcookie('remember_token', $token, [
'expires' => time() + 60 * 60 * 24 * 30,
'path' => '/',
'secure' => true, // HTTPS only - the cookie never rides plaintext
'httponly' => true, // invisible to JavaScript - XSS can't exfiltrate it
'samesite' => 'Lax', // not sent on cross-site subrequests
]);
What each flag actually buys you
httponly is the XSS damage-limiter: script injection on your page can do many bad things, but reading this cookie isn't one of them. Auth material has no business being readable from JS - if frontend code "needs" the session id, the design is wrong, not the flag.
samesite is CSRF's structural counter. Lax (the modern browser default, but declare it - defaults differ and change) withholds the cookie from cross-site POSTs and iframes while allowing top-level link navigation: right for sessions on sites people arrive at via links. Strict withholds it even for link clicks - users arriving from email look logged-out on first load - so reserve it for step-up cookies guarding sensitive actions (admin re-auth, payment confirmation). None requires Secure and is only for genuine cross-site embedding. SameSite is belt and braces territory: keep CSRF tokens too, because old browsers and subdomain edge cases exist.
The prefix trick almost nobody ships: __Host-
setcookie('__Host-session', $sessionId, [
'expires' => 0, // session cookie
'path' => '/', // REQUIRED by the prefix
'secure' => true, // REQUIRED by the prefix
'httponly' => true,
'samesite' => 'Lax',
// note: NO 'domain' - also required by the prefix
]);
A cookie whose name starts with __Host- is rejected by the browser unless it's Secure, path=/, and domain-less (host-locked). That's enforcement of your policy by the user agent itself - crucially defeating cookie tossing, where a compromised subdomain (or a sibling site on shared hosting) plants a lookalike session cookie for the parent domain. With the prefix, evil.example.com cannot forge __Host-session for example.com; the browser refuses it. Rename your session cookie once (session_name('__Host-session') for native sessions, plus matching cookie params) and an entire attack family exits the threat model.
Housekeeping notes
- Deleting: set the same name with the same path/domain attributes and an expiry in the past - mismatched attributes are why "the logout cookie won't die".
- Native sessions take the same options via session_set_cookie_params([...]) - do it before session_start, and revisit session locking while you're in that file.
- Size: ~4KB per cookie, sent on every request - cookies are for identifiers, not data. State lives server-side or in localStorage per its trust level.
One call, five options, one funny name prefix - and the cookie layer stops being the soft underbelly it is on most PHP sites.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.