1
0
Files
spring-security-demo/cors-csrf/docs/07-samesite.md
2026-08-28 10:16:21 +05:30

112 lines
4.8 KiB
Markdown

# 7. SameSite, `Secure`, and the cookie that is never stored
*Prev: [6. CSRF for SPAs](06-csrf-for-spas.md) · Next: [8. Debugging recipes](08-debugging-recipes.md)*
CORS decides whether the browser lets your JavaScript *read* a response. SameSite decides whether
the browser *sends the cookie* in the first place. Getting CORS perfect and SameSite wrong
produces a request that arrives cleanly and is anonymous.
## What Spring emits by default
From [`docs/output/12-samesite.txt`](output/12-samesite.txt), under `csrf.spa()`:
```
Set-Cookie: XSRF-TOKEN=<token>; Path=/
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
```
The session cookie gets `SameSite=Lax` from Boot's
`server.servlet.session.cookie.same-site` default. The CSRF cookie gets **no SameSite attribute
at all**: `CookieCsrfTokenRepository`'s default cookie customizer is, in bytecode, a single
`return`. Nothing is set.
An absent `SameSite` is not "no restriction". Chromium-based browsers treat it as `Lax`; Firefox has
**not** enabled Lax-by-default on its release channel (`network.cookie.sameSite.laxByDefault` is on in
Nightly only). The two disagree, which is why "it works in Firefox and not in Chrome" is so often a
missing `SameSite` attribute. `SpecCookieJar` models the Chromium behaviour, because that is the one
you have to survive.
## The two rules that matter
**Storage (RFC 6265bis &sect;5.5).** *"If the cookie's `same-site-flag` is `None` and the
cookie's `secure-only-flag` is false, then abort these steps and ignore the newly created cookie
entirely."*
`SameSite=None` without `Secure` is not a weaker cookie. It is not a cookie. No console warning
is required, no error is raised, and the server has no idea.
**Sending (RFC 6265bis &sect;5.8.3).** `Strict` and `Lax` cookies are not attached to cross-site
requests, except that `Lax` allows top-level safe-method navigations. A `fetch()` from a SPA is a
subresource request, not a top-level navigation, so `Lax` does not help it.
## Running the rules instead of quoting them
`SpecCookieJar` implements those two paragraphs in about sixty lines, and
`/diag/cookie-spec` feeds the application's own `Set-Cookie` headers through it. Over a
trustworthy origin:
| `Set-Cookie` | Stored? | Sent on a cross-site `fetch`? |
|---|---|---|
| `JSESSIONID=s1; HttpOnly; SameSite=Lax` | yes | no |
| `JSESSIONID=s2; HttpOnly; SameSite=None` | **no** | &mdash; |
| `JSESSIONID=s3; Secure; HttpOnly; SameSite=None` | yes | **yes** |
| `XSRF-TOKEN=t1` (no SameSite) | yes | no |
| `XSRF-TOKEN=t2; SameSite=None` | **no** | &mdash; |
| `XSRF-TOKEN=t3; Secure; SameSite=None` | yes | **yes** |
Two of six reach a cross-site fetch, and they are the two carrying both attributes.
## The trap that costs a day: plain `http` during development
`Secure` is only honoured from a *trustworthy* origin. Over plain `http` the attribute is
discarded, which makes `SameSite=None; Secure` collapse into `SameSite=None` with no `Secure`
&mdash; which is then rejected outright. The first block of `12-samesite.txt` is that: **nothing
survives**.
`http://localhost` is treated as trustworthy by current browsers, so it works. `http://127.0.0.1`
and `http://192.168.x.x` are not, and do not. A developer testing a cross-site SPA against a LAN
address will find that the cookie simply never appears, with no message anywhere.
## The configuration
Boot writes exactly what you tell it, and does **not** add `Secure` for you when you ask for
`none`:
```yaml
server:
servlet:
session:
cookie:
same-site: none
secure: true # required. Omit it and the cookie is discarded by the browser.
http-only: true
```
Spring Security's CSRF cookie is separate and needs its own customizer:
```java
CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse();
repository.setCookieCustomizer((cookie) -> cookie.sameSite("None").secure(true));
http.csrf(csrf -> csrf.spa().csrfTokenRepository(repository));
```
Order matters &mdash; chapter 6.
## Partitioned cookies (CHIPS)
`Partitioned` requires `Secure` and, in practice, `SameSite=None`. It changes the cookie's
storage key so that a cookie set in a third-party context is scoped to the top-level site that
embedded it. `SpecCookieJar` models the `Secure` requirement; it does not model partitioning,
which is noted here rather than pretended.
If your SPA and API are separate registrable domains, the honest conclusion is:
> **A same-site deployment removes this entire chapter.** Serving the SPA and the API from one
> origin, or from two subdomains of one registrable domain, means `SameSite=Lax` works, `Secure`
> is a hygiene setting rather than a prerequisite, and the preflight disappears. A reverse proxy
> in front of both is usually less work than everything above.
---
*Prev: [6. CSRF for SPAs](06-csrf-for-spas.md) &middot; Next: [8. Debugging recipes](08-debugging-recipes.md)*