read.markets/app
Giorgio Gilestro 62960d5bea 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>
2026-05-26 19:14:17 +02:00
..
jobs test+fix: make the suite run cleanly in the test container 2026-05-26 00:11:18 +02:00
routers security: stop localStorage leaking portfolios across users 2026-05-26 19:14:17 +02:00
services pricing: land £7/£70 paid tier and make behaviour match 2026-05-26 11:34:37 +02:00
static pricing: land £7/£70 paid tier and make behaviour match 2026-05-26 11:34:37 +02:00
templates security: stop localStorage leaking portfolios across users 2026-05-26 19:14:17 +02:00
__init__.py initial commit — cassandra v0.1 2026-05-15 21:56:10 +01:00
auth.py public: landing + pricing + legal pages, apex-ready, lawyer-reviewed 2026-05-24 00:08:02 +02:00
branding.py public: landing + pricing + legal pages, apex-ready, lawyer-reviewed 2026-05-24 00:08:02 +02:00
cli.py cli: send-test-digest for previewing digest emails 2026-05-25 23:30:33 +02:00
config.py stripe: wire checkout, customer portal, and webhook for read.markets 2026-05-26 18:45:13 +02:00
db.py sync: encrypted cloud backup for portfolios + settings UX rework 2026-05-23 16:15:54 +02:00
logging.py initial commit — cassandra v0.1 2026-05-15 21:56:10 +01:00
main.py stripe: wire checkout, customer portal, and webhook for read.markets 2026-05-26 18:45:13 +02:00
models.py stripe: wire checkout, customer portal, and webhook for read.markets 2026-05-26 18:45:13 +02:00
redis_client.py phase G: data minimisation + passwordless auth + DeepSeek-first LLM 2026-05-18 14:16:57 +01:00
scheduler_main.py scheduler: register email_digest_job at 06:30 UTC 2026-05-25 23:20:06 +02:00
schemas.py news: auto-tag headlines + market-aware cadence + filter UI 2026-05-21 23:25:03 +01:00
templates_env.py beta: header chip flagged by BETA_MODE config (default on) 2026-05-25 22:42:19 +02:00