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