Comparisons
Pairs that get conflated in real conversations, and in real pull requests. Neither column wins — what decides is the requirement. Each record leads with the confusion, because the confusion is the reason the record exists.
Cookies vs Script-readable token storage
The argument is usually reduced to "localStorage is vulnerable to XSS" versus "cookies are vulnerable to CSRF", as if you were picking which vulnerability to have. What is actually being traded is *who attaches the credential*. A cookie is attached by the browser, automatically, on requests to its domain — which is convenient, and is precisely why cross-site requests carry it and CSRF exists. A script-readable token is attached by your code, so nothing is sent unless you send it, and CSRF largely disappears — but the token now lives somewhere any script running on your origin can read, which means an XSS becomes credential theft rather than session abuse. It is worth being exact about the XSS case: if an attacker is executing script on your origin, a HttpOnly cookie is not safe either, because they can make authenticated requests as the user from inside the page. What HttpOnly buys is that the credential cannot be *exfiltrated* and reused elsewhere, later, without the page. That is a real difference, and it is smaller than it is usually claimed to be.
First-party web apps talking to their own origin or a sibling one, where you want the credential outside JavaScript's reach and you are willing to handle CSRF.
Clients that must attach a credential explicitly — cross-origin APIs, multiple backends, or flows where automatic attachment is exactly what you do not want.
| Dimension | A cookie the script cannot read (HttpOnly, Secure, SameSite) | A token in memory, web storage or IndexedDB, attached by your code |
|---|---|---|
| Attached by | The browser, automatically | Your code, explicitly |
| Readable by script | No, when HttpOnly | Yes, by any script on the origin |
| CSRF exposure | Yes — mitigated by SameSite plus a token | Largely none, because nothing is sent automatically |
| XSS exposure | Attacker can act as the user in-page, but cannot take the value | Attacker can take the value and use it anywhere |
| Cross-origin use | Needs credentialed CORS and an exact origin | Straightforward — it is just a header |
| Survives a reload | Yes, until expiry | Only if persisted, which is the risky part |
| Revocation | Server clears or rotates the session | Needs short expiry plus refresh, or a denylist |
| Sent on every request to the domain | Yes, including ones that did not need it | Only where you attach it |