Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

SameSite, HttpOnly and the __Host- Prefix: Cookies Done Properly

C
Carlo Fontanos
· 3 min read

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.

C
Written by Carlo Fontanos

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.

Message sent!

Your details are only used to reply to you.

Keep reading