The Session Token in localStorage That Outlived the XSS
The token was readable by any script on the origin, so the injection took it and used it from elsewhere, and clearing the cookie would not have mattered…
I started at the incident, because that is where the symptom landed, and walked it backwards link by link until the found stopped looking innocent.
Strip the token in script-readable storage down and one property makes it worse than the cookie: the token leaves the browser, so revoking the browser's copy does nothing, and the stolen token works from anywhere until it expires.
I have since met the same mechanics in plenty of origin that stores a session credential where JavaScript can read it.
Why localStorage is the wrong place
The localStorage is the script's storage, and the script's is the any script, and the any script is the same origin, and the same origin is the third party's bundle, and the bundle is the injection's surface, and the surface is the read, and the read is the token, and the token is the session, and the session is the identity, and the identity is the theft.
The cookie with the HttpOnly flag is the not readable by the script, and the not readable is the injection's limit, and the limit is the action without the credential, and the credential is the persistence, and the persistence is the elsewhere, and the elsewhere is the HttpOnly's prevention, and the prevention is the fix, and the fix is the storage's choice.
The trade is the CSRF, and the CSRF is the cookie's risk, and the risk is the SameSite's mitigation, and the mitigation is the available, and the available is the reason the cookie is the better trade, because the CSRF is preventable and the exfiltration is not recoverable.
Why the patch did not end it
The injection was the patched, and the patched was the vector's closure, and the closure was the future's prevention, and the prevention was the not the past, and the past was the taken token, and the taken was the valid, and the valid was the expiry, and the expiry was the days, and the days was the window, and the window was the attacker's, and the attacker's was the incident.
The revocation was the needed, and the needed was the server's session invalidation, and the invalidation was the token's death, and the death was the fix, and the fix was the not automatic, because the stateless token has no server-side list, and the no list is the no revocation, and the no revocation is the design's cost, and the cost is the incident's duration.
This is the same persistence as the secrets in git history that are still there, and the common root is the property, not the platform.
The fixes
Move the session to an HttpOnly, Secure, SameSite cookie. The cookie is the script's unreadable, and the unreadable is the injection's limit, and the limit is the fix, and the fix is the storage, and the storage is the server's set-cookie, and the set-cookie is the discipline, because the credential should not be where the injected script runs.
Keep the access token short-lived. The minutes are the window, and the window is the stolen token's life, and the life is the damage's bound, and the bound is the fix, and the fix is the expiry, and the expiry is the refresh, and the refresh is the cookie-held, and the cookie-held is the discipline.
Make the refresh token revocable server-side. The refresh is the stored, and the stored is the list, and the list is the revocation, and the revocation is the incident's end, and the end is the fix, and the fix is the database row, and the row is the discipline, because the stateless token cannot be revoked and the revocation is the incident's requirement.
Rotate on the suspected compromise. The rotation is the invalidation, and the invalidation is the all sessions, and the sessions are the user's, and the user's is the inconvenience, and the inconvenience is the cheaper than the breach, and the cheaper is the fix, and the fix is the runbook, and the runbook is the discipline.
Add a Content Security Policy. The CSP is the injection's limit, and the limit is the inline script's block, and the block is the mitigation, and the mitigation is the depth, and the depth is the second layer, and the layer is the fix, and the fix is the header, and the header is the discipline, because the CSP does not prevent the theft but reduces the injection's reach.
What I now do
A session token in localStorage is readable by any script on the origin, so an injection exfiltrates it and it keeps working from elsewhere until it expires. Store the session in an HttpOnly Secure SameSite cookie, keep the access token short-lived, make the refresh token revocable, rotate on suspected compromise, and add a CSP.
The XSS was patched and the incident was not over, because the token had been read out of localStorage and was still valid from the attacker's machine, and there was no server-side session list to revoke it against. I now check for that property first, because it is where the failure actually lives.
Published on debugly.dev by Rohit Bhadani | Founder of Krova Cloud