Add the cors-csrf module
This commit is contained in:
111
cors-csrf/docs/07-samesite.md
Normal file
111
cors-csrf/docs/07-samesite.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 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 §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 §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** | — |
|
||||
| `JSESSIONID=s3; Secure; HttpOnly; SameSite=None` | yes | **yes** |
|
||||
| `XSRF-TOKEN=t1` (no SameSite) | yes | no |
|
||||
| `XSRF-TOKEN=t2; SameSite=None` | **no** | — |
|
||||
| `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`
|
||||
— 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 — 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) · Next: [8. Debugging recipes](08-debugging-recipes.md)*
|
||||
Reference in New Issue
Block a user