Browser security boundaries
Protect session cookies
Use restrictive cookie attributes and understand why a cookie can be sent even when browser JavaScript cannot read it.
A session cookie is authority. Whoever holds it is the logged-in user until it expires. So the attributes you set on that cookie decide how far the authority can leak.
HttpOnly hides the cookie from document.cookie. A script can’t read it. But HttpOnly does nothing to stop the browser from attaching the cookie to requests. Reading and sending are separate concerns, and each attribute controls a different one. Keep that split in mind for the whole lesson.
Set the attributes on purpose
Here is a session cookie set from an Express handler. Every attribute is there for a reason:
res.cookie('session', sessionId, {
httpOnly: true, // no script access
secure: true, // only sent over HTTPS
sameSite: 'lax', // not sent on cross-site subrequests
path: '/', // scope to what the app needs
maxAge: 1000 * 60 * 60, // 1 hour
})
Notice there is no Domain attribute. Leave it out and the cookie is scoped to the exact host that set it. Add Domain=.flaviocopes.com and every subdomain receives it, including the one a coworker spins up for a demo. That is authority you rarely want to hand out.
You can check what the browser received with curl:
curl -si https://app.flaviocopes.com/login -d 'user=flavio' | grep -i set-cookie
You should see one line, and all the attributes you asked for:
Set-Cookie: session=abc123; Max-Age=3600; Path=/; HttpOnly; Secure; SameSite=Lax
Let the browser enforce it
When the rules fit, a __Host- prefix is the strongest baseline the browser will enforce for you:
Set-Cookie: __Host-session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/
The __Host- prefix requires Secure, Path=/, and no Domain. If any of those is missing, the browser drops the cookie. A policy in a document becomes something the browser checks on every response. I like that a lot more than a code review comment.
What goes wrong
The classic mistake is HttpOnly without Secure. A stray http:// link in an email sends the cookie in clear text before your redirect to HTTPS ever runs. The fix is one attribute, and the __Host- prefix makes forgetting it impossible.
The other common one is SameSite=Strict breaking a real flow. A user comes back from an external login provider, the browser treats that as a cross-site navigation, and the cookie is not sent. They land on your site logged out. Lax is usually the right answer, but test the real sign-in return path before you pick.
Try this on your own project: capture the session Set-Cookie header and justify every attribute out loud, including Domain and expiry. Then send an http:// request, submit a form from another site, and complete a real sign-in. The cookie should show up only where you expect it.
Lesson completed