security: stop localStorage leaking portfolios across users
Bug: the per-browser pie was stored under a single global key
(`cassandra.pie`) with no per-user scope. If User A uploaded a
portfolio and User B then signed in on the same browser, User B saw
User A's holdings — portfolio.js read straight from localStorage on
hydration with no check that the data belonged to the current session.
This was not a server-side leak: the session cookie was correct, no
API returned User A's data to User B. The stale browser state was the
sole vector. Reported by the operator while testing the paid-checkout
flow with a second account on the same browser.
Fix — defense in depth, two layers:
1. base.html now stamps cu.user.id into localStorage as
`cassandra.user_id` on every authenticated page load. If the
previous stamp doesn't match the current user, wipe localStorage
(preserving only `cassandra.theme`, which is cosmetic) and
sessionStorage before any other script runs. This catches:
- the reported scenario (User A logs out, User B logs in)
- any case where logout missed the wipe (JS disabled, browser
killed before the redirect ran)
- cookie-revocation / session-rotation edge cases where the
server-side identity changes without an explicit logout
2. /logout no longer returns a bare 303; it returns a small HTML
page that actively wipes per-user localStorage + sessionStorage
client-side (theme preserved), then redirects to /login. A
meta-refresh covers the no-JS case (the cookie deletion is
still server-side, so security is preserved either way).
Behaviour for the legitimate case (same user logs out + back in)
is unchanged: their localStorage data survives because the
mismatch check sees the same user_id and doesn't fire — the
logout wipe runs but they re-stamp + re-upload only the
cassandra.user_id and a fresh pie cycle if they choose to upload.
Suite: 221 passed, 5 skipped, 0 failed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
410afe0078
commit
62960d5bea
2 changed files with 47 additions and 1 deletions
|
|
@ -4,6 +4,29 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{% block title %}{{ BRAND_NAME }}{% endblock %}</title>
|
||||
{# Cross-user contamination guard.
|
||||
|
||||
localStorage is browser-wide; if User A uploads a portfolio and User B
|
||||
logs in on the same browser, the stale `cassandra.pie` would otherwise
|
||||
render as User B's holdings. We stamp the logged-in user's id in
|
||||
localStorage on every authenticated page load and wipe per-user keys
|
||||
if the id changed since last time. Theme stays — it's cosmetic. #}
|
||||
<script>
|
||||
(function() {
|
||||
try {
|
||||
var current = "{{ cu.user.id if cu and cu.user else '' }}";
|
||||
if (!current) return;
|
||||
var last = localStorage.getItem('cassandra.user_id') || '';
|
||||
if (last && last !== current) {
|
||||
var theme = localStorage.getItem('cassandra.theme');
|
||||
localStorage.clear();
|
||||
if (theme) localStorage.setItem('cassandra.theme', theme);
|
||||
try { sessionStorage.clear(); } catch (e) {}
|
||||
}
|
||||
localStorage.setItem('cassandra.user_id', current);
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
{# Apply saved theme before stylesheet renders to avoid a flash. #}
|
||||
<script>
|
||||
(function() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue