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.Strictnever sends it cross-site.Nonealways does and requiresSecure.Max-AgeorExpires: 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.comshares 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.