Cookies and sessions

HTTP & the web · Practical · 12 min

Logins work through cookies, and most session-hijacking and CSRF bugs are cookie attribute mistakes.

HTTP is stateless, so servers hand clients a cookie to send back. The server responds with Set-Cookie: session=abc123; ..., and the browser includes Cookie: session=abc123 on later requests to that site. A session usually means the cookie holds a random ID that maps to data on the server, or a signed token holding the data itself.

The attributes decide security:

  • HttpOnly: JavaScript can't read it (document.cookie), so an XSS bug can't steal the session. Always set it on session cookies.
  • Secure: only sent over HTTPS.
  • SameSite: whether it's sent on cross-site requests. Lax (Chrome's default when the attribute is missing; set it explicitly) sends it on top-level navigations but not on cross-site POSTs or embedded requests, which blocks most CSRF. Strict never sends it cross-site. None always does and requires Secure.
  • Max-Age or Expires: without them it's a session cookie, deleted when the browser session ends.
  • Domain: omit it to restrict the cookie to the exact host; Domain=example.com shares it with all subdomains.
  • Path: limits it to URL paths; not a security boundary.

The __Host- name prefix enforces Secure, no Domain and Path=/, the strongest setting.

Cookies are sent with every matching request, so keep them small (browsers cap each at about 4 KB). Third-party cookies, set by embedded sites, are restricted or blocked in Safari and Firefox and limited in Chrome; don't build new features on them.

Example

import http from "node:http";

const server = http.createServer((req, res) => {
  if (req.url === "/login") {
    res.setHeader("Set-Cookie",
      "__Host-session=r4nd0m1d; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=3600");
    return res.end("logged in");
  }
  res.end(`you sent: ${req.headers.cookie ?? "no cookie"}`);
});

server.listen(0, async () => {
  const base = `http://localhost:${server.address().port}`;
  const r = await fetch(base + "/login");
  console.log(r.headers.get("set-cookie"));
  // __Host-session=r4nd0m1d; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=3600
  const r2 = await fetch(base + "/me", { headers: { Cookie: "__Host-session=r4nd0m1d" } });
  console.log(await r2.text());   // you sent: __Host-session=r4nd0m1d
  server.close();
});

Remember

  • Set-Cookie stores, Cookie sends back
  • Session cookies: HttpOnly; Secure; SameSite=Lax at minimum
  • SameSite blocks most CSRF; None requires Secure
  • Omit Domain to keep a cookie on one host

Watch out for

  • Storing session tokens in localStorage, readable by any injected script, instead of an HttpOnly cookie.

Check yourself

  1. Which attribute stops JavaScript running on the page from reading a cookie?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies