Session cookies are the keys to a logged-in account. If one leaks, whoever holds it is the user. A handful of cookie attributes decide how easily that can happen: whether the cookie travels over plain HTTP, whether JavaScript can read it, and whether other sites can trigger requests that carry it. This guide explains each flag, the SameSite behavior that surprises developers, how to set them in popular stacks, and how to debug a cookie that vanishes.
Anatomy of a Set-Cookie header
A server creates a cookie with a Set-Cookie response header:
Set-Cookie: session=9f2c1a7e; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=86400
The part before the first semicolon is the name and value. Everything else is an attribute that tells the browser how to store and send it. The browser does not return attributes to the server; on later requests it sends only Cookie: session=9f2c1a7e. That means you can only see attributes in the Set-Cookie response, in DevTools, or with the cookie checker.
Secure
The Secure attribute tells the browser to send the cookie only over HTTPS. Without it, a cookie set for example.com is also sent on any plain HTTP request to the same host, where a network attacker can read it. That includes the first HTTP request a user makes before your redirect to HTTPS happens.
Notes:
- Set
Secureon every cookie on an HTTPS site, not just the session cookie. - Browsers refuse to set a
Securecookie from an insecure origin, with the exception oflocalhostin most modern browsers, which is convenient for development. Securecomplements HSTS. HSTS stops the HTTP request being made at all,Securestops the cookie being sent if one is made. See the HSTS preload guide.- Behind a reverse proxy or load balancer that terminates TLS, many frameworks only mark cookies
Secureif they believe the request was HTTPS. If your app sees plain HTTP from the proxy, it may omit the flag. Configure the trusted proxy headers (X-Forwarded-Proto) properly.
HttpOnly
HttpOnly hides the cookie from JavaScript. document.cookie will not list it, and scripts cannot read or overwrite it. The point is to limit the impact of cross-site scripting: if an attacker injects script, they cannot simply read the session ID and send it to their server.
It is not a complete defense. A script running on your page can still make authenticated requests with the cookie attached, because the browser adds it automatically. But it blocks the most common theft scenario and costs nothing, so session cookies and any other cookie that only the server needs should be HttpOnly.
Leave HttpOnly off only for cookies that your frontend code genuinely must read, such as a UI theme preference. Never put anything secret in such a cookie.
SameSite
SameSite controls whether the browser attaches the cookie to requests that start on a different site. It is the primary defense against cross-site request forgery (CSRF) at the browser level and it limits tracking by third parties.
| Value | Cookie sent on cross-site requests? | Typical use |
|---|---|---|
Strict | Never. Not even when a user clicks a link from another site | Banking, admin sessions |
Lax | Only on top-level navigations using safe methods (GET), such as clicking a link | Default for login sessions |
None | Always, but requires Secure | Embedded widgets, cross-site APIs, third-party contexts |
Important behavior:
- Default is Lax in modern browsers. If you do not specify SameSite, Chrome and other Chromium browsers treat the cookie as
Lax. Do not depend on the default; set it explicitly so behavior is consistent and reviewable. SameSite=NonewithoutSecureis rejected. The cookie is silently dropped. The console shows a warning about the cookie being blocked.- "Site" is not "origin". Same-site means the same registrable domain (for example
example.com), soapp.example.comandwww.example.comare same-site, andexample.comandexample.netare not. Cross-origin but same-site requests include SameSite cookies. - Strict can feel broken. With
Strict, a user following a link to your site from an email or chat app arrives without the session cookie and looks logged out until they navigate again. Many sites useLaxfor the main session andStrictfor high-risk cookies.
SameSite reduces CSRF risk but does not replace anti-CSRF tokens for state-changing requests, especially while you must support older browsers.
Domain, Path and lifetime
Domain. If you omit it, the cookie is host-only: sent only to the exact host that set it. If you setDomain=example.com, it is also sent to all subdomains. Prefer host-only cookies. A broad domain cookie is exposed to every subdomain, including any that run weaker code or could be taken over. See subdomain takeover and dangling DNS.Path. Restricts the cookie to a URL prefix. It is a convenience, not a security boundary, because other paths on the same origin can still access it from script unlessHttpOnlyis set.Max-AgeandExpires. Without either, the cookie is a session cookie that goes away when the browser closes (though browsers with session restore may keep it). UseMax-Agefor clarity. Chrome limits cookie lifetime to 400 days regardless of what you ask for.
Cookie prefixes
Prefixes add constraints the browser enforces on the cookie name:
__Secure-requires theSecureattribute and a cookie set from a secure origin.__Host-requiresSecure,Path=/, and noDomainattribute, locking the cookie to the exact host.
Set-Cookie: __Host-session=9f2c1a7e; Path=/; Secure; HttpOnly; SameSite=Lax
__Host- stops a sibling subdomain from planting or overwriting your session cookie, a technique called cookie tossing. It is the strongest simple setting for a session cookie on a single host.
Partitioned cookies (CHIPS)
Browsers are restricting third-party cookies. If your service runs inside an iframe on other sites (a chat widget or embedded payment form) and needs state, the Partitioned attribute opts a cookie into per-top-level-site storage. It must be combined with Secure and is typically used with SameSite=None. Support varies by browser, so check current compatibility data before relying on it.
Setting flags in common stacks
PHP, for the session cookie:
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
Or globally in php.ini:
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = "Lax"
Express (Node.js) with express-session:
app.use(session({
name: '__Host-sid',
secret: process.env.SESSION_SECRET,
cookie: { secure: true, httpOnly: true, sameSite: 'lax', maxAge: 86400000 },
resave: false,
saveUninitialized: false,
}));
app.set('trust proxy', 1); // when behind a TLS-terminating proxy
Django in settings.py:
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_SECURE = True
WordPress sets its own cookies, and the usual fix is enabling HTTPS everywhere and using a hardening plugin or wp-config.php constants. See the WordPress security hardening checklist.
At the web server or proxy level, you can rewrite flags on cookies from a legacy app. For nginx with the proxy module this can be done with proxy_cookie_flags:
proxy_cookie_flags ~ secure httponly samesite=lax;
Debugging cookies that go missing
- Inspect the raw header.
curl -sI https://example.com/login | grep -i set-cookieshows exactly what the server sends. Use the HTTP header checker or cookie checker if you prefer a UI. - Open DevTools, Application, Cookies. Chrome shows each cookie with its flags and, in the Issues panel, why a cookie was blocked.
- Look for the cross-site cause. Login through a third-party identity provider, a payment redirect, or an embedded iframe often fails because the return trip is a cross-site POST, which
Laxdoes not carry. UseSameSite=None; Securefor the specific cookie that must survive, and keep the restLax. - Check the scheme. A cookie marked
Securewill not be set or returned onhttp://pages, which is often the hidden reason a dev environment behaves differently from production. - Check the size and count. Browsers limit a cookie to about 4 KB and cap the number per domain. Oversized cookies are dropped or can cause server errors on the next request.
- Check domain mismatches. A cookie set for
www.example.comis not sent toexample.com, and the reverse only holds ifDomainis set.
A baseline policy
For a typical site: session cookie __Host- prefixed, Secure; HttpOnly; SameSite=Lax; Path=/, short idle timeout, regenerated at login, and a CSRF token on state-changing forms. Anything cross-site gets SameSite=None; Secure only where required. Back this with HTTPS everywhere, HTTP security headers, and a Content Security Policy.
Next steps
- Scan your site's cookies and flags with the cookie checker.
- Confirm the raw headers with the HTTP header checker and the security headers checker.
- Make sure HTTPS is enforced with the HSTS checker.
- Review the rest of the set in the HTTP security headers guide.
FAQ
Do I need Secure if I use HSTS?
Yes. HSTS protects future connections after the browser has seen it, and Secure protects the cookie itself. Use both; they cover different failure modes.
Why did my cookie disappear when I added SameSite=None?
SameSite=None requires Secure. Without it, modern browsers reject the cookie. Serve the site over HTTPS and add Secure.
Does HttpOnly stop XSS?
No. It stops scripts from reading the cookie, but injected script can still perform actions as the user. It reduces damage; fixing XSS and adding a CSP prevents the attack.
Should analytics cookies be HttpOnly?
Usually not, because client-side analytics code needs to read them. Keep sensitive session and authentication cookies separate from these.
Get notified when this guide updates
Plus occasional practical tips on uptime and monitoring, nothing else. Unsubscribe anytime.
This page is general information written by our editorial team and updated when documentation changes. Servers and providers differ, so test changes safely first. See our editorial policy and disclaimer.