Skip to main content
Saved

Pattern Security Advanced

SameSite Cookie

The SameSite attribute on a cookie controls whether the browser attaches it to cross-site requests, which cuts off the mechanism that CSRF attacks rely on.

Examples
native web
Read
5 min

The problem

Browsers attach your cookies to every request that targets your domain, whichever site started that request. If a user visits a malicious page while logged into your application, any request that page makes to your server, such as a hidden form submission or an image tag, includes the user’s session cookie automatically. To your server, that request looks just like a genuine one, because it includes valid credentials.

The pattern

The SameSite cookie attribute tells the browser when it’s allowed to send a cookie on a cross-site request. The browser enforces it, so it works without any token validation in your application code. The attribute takes three values, listed here from most to least strict.

Problem

A cross-site request forgery (CSRF) attack abuses this behavior. An attacker embeds a form that POSTs to /account/transfer or /settings/email, the victim’s browser includes the session cookie, and the action runs under the victim’s identity without them knowing. The user never clicked anything that looked dangerous, but a page they had no reason to trust was able to use their logged-in session. You want a cookie that the browser only sends with requests from your own site, so your users’ credentials never go along with a request that another site started.

Solution

Strict is the most secure. The cookie is never sent on any cross-site request, including top-level navigations. If a user clicks a link to your app from an email or another website, the browser doesn’t send the cookie, so the user arrives logged out even though they have a valid session. That makes CSRF practically impossible, but it’s awkward for people clicking through from search results or a newsletter, who land on a logged-out page and have to navigate again before your app shows them as logged in.

Lax suits most session cookies, and Chrome and other Chromium-based browsers apply it when no SameSite value is specified. The cookie is sent on same-site requests and on top-level GET navigations from other sites, such as someone clicking a link to your app, but it isn’t sent with cross-site subrequests like form POSTs, AJAX calls, images, and iframes. That stops the kinds of request a CSRF attack uses, and links into your app from other sites still work. Someone clicking through from an email arrives logged in, but a hidden form on an attacker’s page can’t send an authenticated request.

With None, the browser sends the cookie on all requests, including cross-site ones. You need it for legitimate cross-site uses, such as third-party embeds or federated SSO. Browsers reject SameSite=None unless the cookie is also marked Secure, so it’s only sent over HTTPS. Choosing None brings back the cross-site risk that the attribute is there to prevent, so you should only use it for cookies that really do need to be sent cross-site.

Whichever value you choose, it works best alongside Secure, which stops the cookie being sent over unencrypted connections, and HttpOnly, which stops JavaScript from reading it, so an XSS attack can’t steal it. SameSite is a strong layer of protection, but it doesn’t cover everything. Some older browsers don’t support it properly, it doesn’t protect endpoints that change state on a GET request, and an XSS attack on your own site gets around it completely, so you still need CSRF tokens on endpoints that change state.

Example

These examples show how to set the attribute directly on the Set-Cookie header, how to configure it from a Node server, and how to pick a value for common use cases.

The attribute is part of the Set-Cookie response header, and you can combine each value with Secure and HttpOnly.

# Strict: never sent cross-site (best for sensitive session cookies)
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly; Path=/

# Lax: sent on top-level GET navigations (the default for most sessions)
Set-Cookie: sessionId=abc123; SameSite=Lax; Secure; HttpOnly; Path=/

# None: sent on all cross-site requests (REQUIRES Secure)
Set-Cookie: widgetPrefs=xyz789; SameSite=None; Secure; Path=/

Setting it from a server

In Express, the sameSite option sets the attribute, and the browser does the enforcing.

// Strict session cookie for a first-party app
res.cookie('sessionId', 'abc123', {
  sameSite: 'strict', // never sent on cross-site requests
  secure: true,       // only sent over HTTPS
  httpOnly: true,     // not readable by JavaScript (mitigates XSS theft)
  maxAge: 1000 * 60 * 60 * 24, // 1 day
});

// Lax suits sessions where inbound links must keep users logged in
res.cookie('sessionId', 'abc123', {
  sameSite: 'lax',
  secure: true,
  httpOnly: true,
});

// None is required for legitimate cross-site cookies (embeds, SSO)
// The browser rejects SameSite=None unless Secure is also set
res.cookie('widgetPrefs', 'xyz789', {
  sameSite: 'none',
  secure: true, // mandatory with sameSite: 'none'
});

Choosing a value

The value you pick depends on what the cookie is used for.

Use caseValueReason
First-party session, high-value appStrictThe strongest CSRF protection, and fine if users don’t need to arrive logged in from links
First-party session, general web appLaxBlocks cross-site subrequests, and users still arrive logged in from links
SSO cookie on a cross-site POST backNoneThe identity provider POSTs back cross-site, as with SAML or form_post, and Secure is required
Third-party embedded widgetNoneThe cookie has to be sent on cross-site requests, and Secure is required
Double-submit CSRF token cookieLaxYour own pages read it and send it back in a header or form field

Benefits

  • It stops the browser from sending cookies with requests that other sites started, which cuts off the mechanism CSRF attacks depend on.
  • The browser enforces it on every request, so there’s no per-route logic or application-level check for you to maintain.
  • Lax is the default in Chromium-based browsers, so cookies get some protection there even if you forget to set the attribute.
  • You set it with a single attribute on the Set-Cookie header, without adding middleware or passing tokens around.
  • Combined with Secure and HttpOnly, the same cookie is also protected from being sent over unencrypted connections and from being stolen through XSS.
  • Strict makes CSRF practically impossible for cookies that never need to be sent cross-site.
  • For low-risk endpoints, you might not need to rely on CSRF tokens as much, which keeps that part of your defense simpler.

Tradeoffs

  • With Strict, users who click through from an email or search results arrive logged out and have to navigate again.
  • None brings back the cross-site risk that the attribute is there to prevent, and it requires Secure, so it only works over HTTPS.
  • SameSite doesn’t give you complete CSRF protection. You still need tokens on endpoints that change state, especially any that do it on a GET request.
  • Older browsers handle SameSite inconsistently, and some legacy clients, such as Safari 12, treat None as Strict or ignore the attribute entirely.
  • app.example.com and blog.example.com count as the same site, so SameSite won’t stop requests from one of your subdomains to another.
  • If you forget Secure on a SameSite=None cookie, the browser silently rejects it, and the cross-site flows that depend on it break in ways that are hard to debug.
  • Different cookies often need different values, so one global setting rarely works for all the cookies in your application.
  • The browser treats a “site” as the registrable domain plus the scheme, which is broader than a full origin, so this might surprise you if you expect the attribute to work per origin.

Summary

The SameSite attribute controls whether the browser attaches a cookie to cross-site requests, and the right value blocks the mechanism that CSRF depends on. Lax is a sensible default for session cookies, Strict works when users don’t need to arrive logged in from links, and None should only be used, always with Secure, for cookies that have to be sent cross-site. It works as a strong layer alongside Secure and HttpOnly, but it doesn’t replace CSRF tokens.

See also

Next in Security Secure Token Storage Keeping authentication tokens in httpOnly cookies with the Secure and SameSite flags stops injected scripts from stealing them.

Newsletter

A monthly email
from Den Odell.

Behind-the-scenes thinking on frontend patterns, site updates, and more.

No spam. Unsubscribe anytime.