Answers · The network and the web
Where should a session ID be stored so JavaScript can't read it?
In shortStore it in an HttpOnly cookie set by the server; that is the only browser storage JavaScript running on the page cannot read.
Store it in an HttpOnly cookie set by the server. That is the only browser storage JavaScript running on the page cannot read, and it is also the storage the browser will attach to requests for you.
What the browser will and will not expose
localStorage is a JavaScript API. If a script is running in your origin, it can call localStorage.getItem('sid') and get the string; the same is true for sessionStorage, a variable in a page script, or a cookie without HttpOnly. The browser has no way to tell your code from an injected dependency, a compromised third-party script, or an extension with page access. HttpOnly is a flag the server sets on the cookie, and the browser then omits that cookie from document.cookie while still sending it on requests. The page cannot read it because the browser refuses, not because your code chooses not to look.
| Where it is kept | Readable by script on the page | Attached by the browser |
|---|---|---|
| HttpOnly cookie | no | yes |
| localStorage | yes | no |
| page variable | yes | no |
| ordinary cookie | yes | yes |
The automatic sending is not a flaw to be avoided. It is the mechanism. When the browser makes a same-origin request it attaches the cookie according to Path, Domain and SameSite, and the server reads it from the Cookie header. A value in localStorage is never attached by the browser; your JavaScript must read it and put it into a header or a body. That means the value has already passed through code on the page, which is exactly the code you are trying to keep it away from.
Take a service with live sessions and 128-bit identifiers. The identifier space is , about . A single random guess hits some live session with probability . At guesses per second, a year gives guesses, and the chance of at least one hit is about . That is why the identifier can be treated as a password and why its storage matters: the guess is not the weak part.
Set-Cookie: sid=K7nQv2Xp...; HttpOnly; Secure; SameSite=Lax;
Path=/; Domain omitted; Max-Age=1209600
HttpOnly page scripts cannot read it
Secure never sent over an unencrypted connection
SameSite=Lax not attached to most requests started by another site
Path=/ sent to the whole application
Domain omitted stays on the exact host, not every subdomain
Max-Age how long the browser keeps it, in secondsThe natural guess, and why it fails
The natural guess is that localStorage is safer because the browser does not send it automatically. That is true and irrelevant. Automatic sending is only dangerous if another site can cause your browser to send the cookie without your page's consent, which SameSite limits. Code already running on your page does not need automatic sending: it can read localStorage directly and send the value wherever it likes. Moving the value into localStorage does not take it out of JavaScript's reach; it puts it in JavaScript's own storage. If an injected script can run, it can read the session and use it. The HttpOnly cookie is the one value that script cannot read, even though it can still cause requests to be made with it.
Two limits are worth stating. HttpOnly protects confidentiality from page code, not integrity: a script that cannot read the cookie can still make requests from the page, and the browser will attach the cookie, so an injection can act as the user for the session's lifetime. SameSite=Lax reduces cross-site requests, and Secure keeps the cookie off unencrypted connections, but neither makes XSS harmless. If your JavaScript genuinely must read the identifier, an HttpOnly cookie is impossible; then the identifier is not secret from page code, and you should treat it as a bearer token with a short life and narrow scope rather than pretend it is protected.
The header should be explicit. HttpOnly, Secure, SameSite=Lax, Path=/, no Domain, and a Max-Age you chose. Max-Age=1209600 is 14 days, since 14×24×60×60=1,209,600. For a session identifier that is long; the number should follow what the session can do, not what is convenient. Issue a fresh identifier at sign-in and discard the old one, so a value planted in the browser beforehand cannot become the session.
Worth keeping
- An HttpOnly cookie is the only browser storage that page scripts cannot read, because the browser enforces the restriction rather than your code.
- The fact that localStorage is not sent automatically is not protection: any script running on the page can read it and send it.
- HttpOnly protects the value from being read by injected code, but it does not stop that code from making requests that use the cookie.
Where this goes next
This question is one step of Proving Who You Are, a course that works through its subject from the beginning. The page that teaches this part of it is Not Asking Again Every Time, and it reads the whole way through whether or not you have an account.
What an account changes is the practice: problems standing under every idea, marked with a reason rather than a tick, in a copy of the course that is yours.
Read that pageThe whole courseOther questions people ask about this
- Does signing out actually invalidate my token on the server?No. Signing out clears your browser's copy of the token, but the server keeps accepting that token until it expires.
- How can another site make a request as me if it can't read my cookies?A foreign page does not need to read your cookie. It makes your browser send a request to the site that owns the cookie, and the browser attaches the cookie automatically.
- Why isn't a password plus a security question really two-factor authentication?A security question is another thing you know, so it is the same kind of proof as a password, not a second kind.
- What does it mean for an API method to be safe?A safe method is one that changes nothing on the server when it is called, so a caller may repeat it freely and a cache or a crawler may use it without anything being altered.
- Why does the TLS handshake take so many round trips before any data is sent?It is not one key exchange. The handshake agrees a secret, proves the server's identity and checks for tampering, and modern TLS fits those into one round trip; a fresh connection still needs three before the page starts.
- If a website has HTTPS, can someone on the same Wi-Fi read my password?No. HTTPS encrypts the form contents and cookies, so someone on the same Wi-Fi cannot read the password itself; they can still see the site, timing and packet sizes.