Set-Cookie HttpOnly: what it blocks in JavaScript (tested in Chrome)
HttpOnly keeps a cookie away from JavaScript while the browser keeps sending it to your server. We tested what page scripts can and can't do with it.
1. Setting it (Node, tested)
res.setHeader("Set-Cookie", [
"session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600",
"theme=dark; Path=/; SameSite=Lax",
]);
2. What JavaScript on the page saw
| Action in the page | Result |
|---|---|
document.cookie | "theme=dark", the session cookie is invisible |
document.cookie = "session=evil; Path=/" | Ignored: session stayed abc123 |
document.cookie = "session=evil2; Path=/; HttpOnly" | Ignored: JavaScript can't create HttpOnly cookies |
document.cookie = "theme=light; Path=/" | Worked (not HttpOnly) |
fetch("/api") | Server received Cookie: session=abc123; theme=light |
So an injected script (XSS) can't read or replace the session token. It can still use it, because the browser attaches the cookie to requests the script makes from your page. HttpOnly limits the damage of XSS; it doesn't prevent XSS. Pair it with a Content-Security-Policy.
3. Python (standard library, tested)
from http.cookies import SimpleCookie
c = SimpleCookie()
c["session"] = "abc123"
c["session"]["httponly"] = True
c["session"]["secure"] = True
c["session"]["samesite"] = "Lax"
c["session"]["path"] = "/"
c["session"]["max-age"] = 3600
print(c.output())
Output: Set-Cookie: session=abc123; HttpOnly; Max-Age=3600; Path=/; SameSite=Lax; Secure. Frameworks have flags for the same thing: Flask/Django httponly=True, PHP setcookie(..., ['httponly' => true]) or session.cookie_httponly=1, Spring ResponseCookie.from(...).httpOnly(true).
4. Which cookies should be HttpOnly
- Session IDs, auth and refresh tokens: always
HttpOnly; Secure; SameSite=Lax(SameSite values measured here). - Cookies your front-end must read (UI theme, consent choice, a CSRF token for double-submit): not HttpOnly.
- A React/Next/Angular app can't read an HttpOnly token, and doesn't need to: send requests with
credentials: "include"(cross-origin) or same-origin and the browser attaches it.
Check which cookies carry the flag in DevTools → Application → Cookies (HttpOnly column) or in the raw Set-Cookie response header.
Frequently asked questions
What does HttpOnly do on a cookie?
It hides the cookie from JavaScript. In our Chrome test document.cookie didn't show it, and scripts could neither overwrite it nor create one, but fetch() still sent it.
Can JavaScript set an HttpOnly cookie?
No. document.cookie = 'name=value; HttpOnly' was ignored entirely in our test. Only the server can set it, with a Set-Cookie header.
Does HttpOnly protect against XSS?
Partly: injected scripts can't steal the token, but they can still make requests that carry it. Use it together with CSP and output escaping.
How do I read an HttpOnly cookie in React?
You don't. Let the browser send it: same-origin requests include it automatically, cross-origin ones need credentials: 'include' and matching CORS headers.
Written with AI assistance. Measured on 2026-10-01 in Chromium 153 (full build): a Node 24 server set the two cookies below, then the page read and wrote document.cookie and made a fetch() while the server logged the Cookie header. The Python snippet was run on Python 3.13. Chromium accepted the Secure cookie over http://127.0.0.1 because it treats 127.0.0.1 as a secure context; on a real domain it needs HTTPS. The Flask/Django/PHP/Spring flag names are from their documentation, not this test. PlainHeaders is our extension; DevTools shows the same in the Application tab, free.