Yellow Theme

Next start · October 2026

Notes / 14

2026-09-15

HttpOnly cookies do not help if /auth/session returns the refresh token

We moved refresh into an HttpOnly cookie so XSS could not read it from storage. Then we watched the network tab: GET /auth/session returned the refresh token in JSON.

Thesis: An HttpOnly cookie only hides what never returns to script. If a session endpoint reads the cookie and echoes the refresh token in the body, XSS that can call that endpoint still owns the session.

What we shipped first

The browser stopped writing tokens into sessionStorage on the happy path. Refresh lived in an HttpOnly cookie. A hard reload called GET /auth/session with credentials included. The green path restored the user.

The response body still included refresh_token. Script that could issue that fetch recovered the long-lived credential. Scoping the cookie to the apex domain widened who might send it. A legacy storage branch waited if the cookie path was ever turned off.

The working shape

Keep refresh on the server side of the cookie jar. Exchange it for short-lived access and id tokens without putting refresh in JSON. Prefer a host-only cookie. Delete the storage fallback on the production path.

# Wrong green: cookie is HttpOnly, JSON still leaks refresh
GET /auth/session  →  { "tokens": { "refresh_token": "…" } }

# Working shape: cookie stays opaque to JS
POST /auth/session/refresh  →  { "tokens": { "access_token": "…", "id_token": "…" } }
# refresh_token never leaves the HttpOnly cookie

Checklist

Try the lab

Curl the session, see what XSS can still steal, patch the BFF, prove refresh never returns in JSON. New here? help. Stuck? help next.

Full page: Open this lab · All labs

Related: the browser does not get the object key; Managed Login only got Privacy and Terms. How we deliver: methodology.

Engineering commentary only — not audit, legal, or certification advice.