Your session cookie is what keeps a user logged in, so whoever holds it is, as far as your site can tell, that user. A handful of attributes on the Set-Cookie header decide whether that cookie can be read by injected JavaScript, sent over plain HTTP, attached to requests from other sites, or overwritten by a subdomain. This guide explains each attribute, the values to use for login sessions and other cookies, and how to set them in the stacks most small sites run on.
Key takeaways
- Give session cookies a __Host- name with Path=/, Secure, HttpOnly and SameSite=Lax, and leave out the Domain attribute.
- HttpOnly stops injected JavaScript reading the session cookie, which greatly limits the damage an XSS bug can do.
- Use SameSite=Lax for normal websites and Strict for admin panels, and always pair SameSite=None with Secure or browsers reject it.
- Set SameSite explicitly rather than relying on browser defaults, and keep using CSRF tokens for state-changing actions.
The recommended session cookie
Here's what a well-configured session cookie looks like, following OWASP's Session Management Cheat Sheet:
Set-Cookie: __Host-session=9f8c2b...e41; Path=/; Secure; HttpOnly; SameSite=Lax
Each part does a separate job:
| Attribute | Stops | Without it |
|---|---|---|
Secure |
Cookie being sent over unencrypted HTTP | Anyone on the network can capture it from an http:// request |
HttpOnly |
JavaScript reading the cookie via document.cookie |
An XSS bug lets an attacker copy the session to their own browser |
SameSite=Lax / Strict |
Cookie being attached to most cross-site requests | Other sites can trigger authenticated requests (CSRF) |
__Host- prefix |
Subdomains or HTTP pages setting or overwriting the cookie | A compromised blog.example.com can plant a cookie for example.com |
No Domain attribute |
Cookie being shared with all subdomains | Every subdomain receives the session |
Secure
Secure tells the browser to send the cookie only over HTTPS, with an exception for localhost during development. Pages served over plain HTTP can't set a Secure cookie at all.
If your site is HTTPS-only, and it should be, there's no reason for any cookie to lack Secure. Pair it with HSTS. HSTS stops browsers from making the HTTP request in the first place, and Secure makes sure the cookie isn't attached if one slips through anyway.
HttpOnly
HttpOnly hides the cookie from JavaScript. The browser still sends it on normal requests, including fetch() and XHR, but document.cookie won't show it. That means an XSS bug can't simply read the session token and send it to an attacker.
It doesn't stop everything. Injected script can still make requests from the victim's browser while the page is open. But it turns "attacker owns the session indefinitely" into "attacker acts only while the victim's tab is open", which is a big reduction in damage.
Use HttpOnly on every cookie your front-end JavaScript doesn't need to read: sessions, remember-me tokens, CSRF secrets that are checked server-side. Cookies your scripts genuinely read, such as a UI theme preference, can go without it.
SameSite
SameSite controls whether the cookie is sent when the request comes from another site:
Strict: sent only on same-site requests. If a user follows a link from an email or another site to yours, the first request arrives without the cookie, so they may look logged out until they click again.Lax: sent on same-site requests and on top-level navigations using safe methods like GET (clicking a link to your site). Not sent on cross-site POSTs, iframes, or image and script requests.None: sent on all requests, including cross-site. Must be combined withSecure, or browsers reject the cookie.
"Site" here means the registrable domain: app.example.com and www.example.com are the same site, while example.com and example.net are not. On shared hosting domains listed in the Public Suffix List, such as github.io, each subdomain counts as a separate site.
What should you use?
- Session cookies on a normal website:
Lax. It blocks the classic cross-site form POST attack and keeps "click a link, arrive logged in" working. OWASP prefersStrictwhere the experience allows it, for example in admin panels or banking. - Cookies that must work inside a third-party iframe or cross-site widget (embedded chat, payment iframes, SSO flows on another domain):
None; Secure. ConsiderPartitionedtoo, which stores the cookie separately per top-level site.
Don't rely on the default. Chrome has treated cookies without SameSite as Lax since Chrome 80, with a temporary allowance for top-level POSTs on very recently set cookies. Other browsers handle it differently. Set the attribute explicitly. And SameSite is defense in depth. OWASP is clear that it doesn't replace CSRF tokens for state-changing actions.
The __Host- and __Secure- prefixes
Cookie prefixes are rules the browser enforces based on the cookie's name:
__Secure-: must be set withSecurefrom an HTTPS page.__Host-: must be set withSecurefrom an HTTPS page, must havePath=/, and must not have aDomainattribute.
The __Host- prefix is the strongest. Because a Domain attribute isn't allowed, the cookie is locked to the exact host that set it, and nothing on a subdomain or an HTTP page can overwrite it. That matters because without the prefix, a compromised or attacker-controlled subdomain can set a cookie for the parent domain and force a victim into a session the attacker controls (session fixation).
Browsers reject cookies that break the prefix rules:
Set-Cookie: __Host-id=1; Secure # rejected: no Path=/
Set-Cookie: __Host-id=1; Secure; Path=/; Domain=example.com # rejected: has Domain
Set-Cookie: __Host-id=1; Secure; Path=/ # accepted
MDN also documents newer __Http- and __Host-Http- prefixes, which additionally require HttpOnly. Browser support for those is newer, so treat them as a bonus on top of the attributes themselves.
A caveat: browsers that don't support prefixes accept the cookie without checking these rules. Always set the attributes properly as well.
Other attributes worth getting right
- Domain: Leave it out for session cookies. Setting
Domain=example.comsends the cookie to every subdomain, including ones run by third parties. - Path: Scope it if it helps you organize, but it isn't a security boundary. Any script on the same host can reach cookies regardless of path.
- Expires / Max-Age: Without either, the cookie is a session cookie. With one, it's persistent. Chrome caps lifetimes at 400 days no matter what you set. Whatever the cookie says, enforce session expiry on the server and invalidate the session at logout.
How to set them
PHP (native sessions)
In php.ini or your pool config:
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = "Lax"
session.use_strict_mode = 1
session.name = __Host-sid
For individual cookies (PHP 7.3+):
setcookie('__Host-prefs', $value, [
'expires' => time() + 60 * 60 * 24 * 30,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
After login, call session_regenerate_id(true) so a session ID set before login can't be reused (session fixation).
Laravel
In config/session.php, or the matching .env values:
'cookie' => '__Host-app_session',
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => 'lax',
'domain' => null,
Node.js (express-session)
app.set("trust proxy", 1); // if behind a proxy or load balancer that terminates TLS
app.use(session({
name: "__Host-sid",
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, sameSite: "lax", path: "/" },
}));
Without trust proxy, Express behind a TLS-terminating proxy thinks the connection is HTTP and won't send a Secure cookie.
Nginx in front of an app you can't change
Nginx 1.19.3+ can add flags to cookies passing through proxy_pass:
location / {
proxy_pass http://app_backend;
proxy_cookie_flags ~ secure httponly samesite=lax;
}
The ~ makes the first argument a regex, so this applies to all cookies. If a front-end script needs to read a particular cookie, add a rule for that cookie name first without httponly. The first matching directive wins.
Check your cookies
Open DevTools > Application (Chrome) or Storage (Firefox) > Cookies. The table shows the Secure, HttpOnly and SameSite columns for every cookie. From a terminal:
curl -sI https://example.com/login | grep -i '^set-cookie'
Check the responses that set the session: the login response, not just the homepage.
Frequently asked questions
Will adding HttpOnly break my site?
Only if your JavaScript reads that cookie directly. Session cookies almost never need to be read by scripts. Search your front-end code for document.cookie before you change cookies it does use.
Should I use SameSite=Strict for everything?
It's the safest option, but users arriving from external links (email, search, social) will look logged out on their first page view. Lax is the usual balance for public sites. Strict suits admin panels and sensitive dashboards.
Why is my SameSite=None cookie being ignored?
It's missing Secure. Browsers reject SameSite=None cookies without it. Also check that the response is served over HTTPS.
Do cookie flags affect my audit grade or SEO?
Cookie attributes have no effect on rankings. They do reduce the impact of XSS and CSRF, two of the most common ways sites get compromised. A compromise is what actually damages search visibility.
What to do next
List the cookies your site sets, mark which ones hold authentication, and give those __Host- names with Secure; HttpOnly; SameSite=Lax. Then reduce the XSS risk that HttpOnly only contains, with a Content Security Policy. To check your HTTPS setup and security headers in one report, run a free audit.
Sources
- MDN: Set-Cookie
- OWASP Session Management Cheat Sheet
- web.dev: SameSite cookies explained
- Chrome for Developers: Cookie Expires and Max-Age attributes now have upper limit
- Nginx: proxy_cookie_flags
Written by the MakeItFlow team
We build the website audit these guides are based on. Every fact is checked against the primary sources listed above . How we research and check
Check your own site in 30 seconds
Run a free audit covering speed, SEO, DNS, email authentication and security headers — and get a prioritised fix list.