# HttpOnly Cookie Attribute: XSS Protection

> Learn how the HttpOnly cookie attribute protects against XSS attacks by preventing JavaScript access to sensitive cookies.

Source: https://howhttpworks.com/cookies/http-only
Last reviewed: 2026-10-04

> **TL;DR:** `HttpOnly` hides a cookie from `document.cookie` so injected JavaScript cannot read or exfiltrate it. It does not stop XSS: the attacker's script can still make authenticated same-origin requests, because the browser attaches the cookie on its own.

## What it does

```http
Set-Cookie: __Host-session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
```

The browser still sends the cookie with every matching request, but it is excluded from `document.cookie` and from the Cookie Store API, and `fetch`/`XMLHttpRequest` never expose `Set-Cookie` response headers to scripts anyway. A script also cannot create or overwrite an HttpOnly cookie via `document.cookie`; the browser ignores the write.

```javascript
// Server sent: Set-Cookie: session=secret; HttpOnly   and   Set-Cookie: theme=dark
console.log(document.cookie)
// "theme=dark"   (session is not listed)
```

## What it does not do

- **It does not stop requests made as the user.** With XSS on your page, `fetch('/api/transfer', { method: 'POST', credentials: 'include', ... })` carries the HttpOnly cookie. `SameSite` does not help because the request is same-site. CSP and output encoding prevent the XSS in the first place; see [Content-Security-Policy](https://howhttpworks.com/headers/content-security-policy).
- **It does not hide the cookie from the user, DevTools, or a network attacker.** Pair it with [Secure](https://howhttpworks.com/cookies/secure) and HTTPS.
- **It does not protect other storage.** Tokens kept in `localStorage` or `sessionStorage` are always readable by script, which is the main reason session identifiers belong in HttpOnly cookies.
- **It does not stop CSRF.** Use [SameSite](https://howhttpworks.com/cookies/same-site) plus CSRF tokens.

## Which cookies should be HttpOnly

Session IDs, refresh tokens and anything the browser merely needs to return to the server: yes. Cookies your frontend must read (for example a double-submit CSRF token the SPA copies into an `X-CSRF-Token` header, or a theme preference) cannot be HttpOnly.

## Framework defaults

| Stack | Setting | Default |
| --- | --- | --- |
| Express (`res.cookie`) | `httpOnly: true` | Off; you must set it |
| express-session | `cookie.httpOnly` | On |
| Django | `SESSION_COOKIE_HTTPONLY` | On |
| Django | `CSRF_COOKIE_HTTPONLY` | Off (the CSRF cookie is meant to be readable unless you use the session-based CSRF token) |
| Flask | `SESSION_COOKIE_HTTPONLY` | On |
| Spring Boot | `server.servlet.session.cookie.http-only` | On |
| Go `net/http` | `http.Cookie{HttpOnly: true}` | Off; you must set it |
| PHP `setcookie` | `'httponly' => true` or `session.cookie_httponly=1` | Off |

```javascript
res.cookie('refresh', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  path: '/auth/refresh',
  maxAge: 7 * 24 * 60 * 60 * 1000
})
```

## Verify

```bash
curl -sI https://example.com/login | grep -i '^set-cookie'
```

In Chrome DevTools, Application, Cookies, the HttpOnly column shows a check mark. In the console, `document.cookie` must not list the cookie name. If it does, the attribute was not on the `Set-Cookie` header, or a second `Set-Cookie` from another layer (a CDN, a different path) set a same-named cookie without it.

## Related

- [Secure](https://howhttpworks.com/cookies/secure)
- [SameSite](https://howhttpworks.com/cookies/same-site)
- [Set-Cookie header](https://howhttpworks.com/headers/set-cookie)
- [Cookie security guide](https://howhttpworks.com/guides/cookie-security)
