From 3c5c0c911ce57b7da00c650d84f34f8ee461e8a6 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 30 Oct 2026 21:32:56 +0100 Subject: [PATCH] =?UTF-8?q?Answer=20product-owner=20round=201:=200=20?= =?UTF-8?q?=F0=9F=94=B4,=202=20=F0=9F=9F=A1=20(0=20misread);=20document=20?= =?UTF-8?q?when=20a=20route=20may=20override?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e4989c1..9bcdd2c 100644 --- a/README.md +++ b/README.md @@ -1385,9 +1385,9 @@ so a stolen cookie jar is worth 30 days of re-mintable access, not ten minutes. obey `SECURE_COOKIES`; the Kratos one takes its flags from Kratos' own config. **The CSRF token isn't bound to the session.** It is a signed double-submit token, so an attacker who -can write cookies on your origin can plant one they know. `SameSite=Lax` and HSTS -(`includeSubDomains`) are what keep that attacker out; it holds while the session cookie stays -`SameSite=Lax` or stricter. +can write cookies on your origin can plant one they know. `SameSite=Lax` keeps a cross-site page +out, and HSTS (`SECURE_COOKIES=true`) a network attacker. A hostile page on a sibling subdomain can +still set cookies, so host nothing untrusted on one. **Offboarding is not instant by default** — a revoked permission or deactivated identity lands within one token TTL, unless the [denylist](#instant-revoke-the-optional-denylist) is on. @@ -1646,14 +1646,15 @@ schedule, and rehearse the restore. Every response carries security headers (`src/http/security-headers.ts`): a strict `Content-Security-Policy` (the core is zero-JS — `script-src 'self'`, no inline scripts), `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY` + `frame-ancestors 'none'`, -`Referrer-Policy`, and HSTS when `SECURE_COOKIES=true`. A page or JSON response is -`Cache-Control: private, no-store`, so a shared cache never keeps one user's page for the next; only -the built-in landing `/`, for a visitor who is signed out and already holds the CSRF cookie, is -shared-cacheable. A route whose page is the same for everyone sets its own `cache-control` via -`RouteResult.headers`. The CSP allows **same-origin** assets only, +`Referrer-Policy`, and HSTS when `SECURE_COOKIES=true`. The CSP allows **same-origin** assets only, so a branding logo must live under `/public/` or be a `data:` URI; a plugin route can override any header per-response via `RouteResult.headers`. +Every page and JSON response is `Cache-Control: private, no-store`, so a cache in front of `web` +never keeps one user's page for the next. A route may override it only for a page that is the same for +every visitor: no user, no CSRF token, no `Set-Cookie`, since the host adds a fresh CSRF cookie to +every plugin route. + A deep link reached while signed out — or after the session JWT lapses mid-task — bounces to the themed sign-in and returns to the **page that was requested** (`return_to`, validated host-relative by `localPath` in `src/http/safe-url.ts`, so a crafted value can't turn login completion into an open