Namespace cost-strip storage per user — fix cross-user leakage - #53
Merged
Conversation
The strip was persisted under two global localStorage keys
(scp.cost.lastEstimate / .lastActual). When user A logged out and user
B logged in on the same browser, restorePersistedCostStrip read those
global keys on user B's first paint and showed user A's last estimate
and actual cost. Same story across browser tabs / restarts.
Three layers so this can't recur:
(1) Keys are now per-user. lastEstimateKey() and lastActualKey() return
"scp.cost.lastEstimate:<username>" and ".lastActual:<username>".
Every read/write site (renderEstimate, renderActualCost, hydrate,
restore, clearPersisted) was switched over.
(2) doLogout wipes EVERY scp.cost.* bucket — every user's, plus the
legacy un-namespaced keys an older build may have left behind. Also
resets the on-screen strip + the in-memory pricing/poll state.
(3) bootstrapPrincipal compares the just-fetched principal name against
the last principal this browser saw (scp.cost.lastPrincipal). If
they differ — even when the previous logout didn't run for whatever
reason — every cost bucket is wiped before restorePersistedCostStrip
has a chance to read.
Bumped app.js?v=24.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The leak
The cost strip persisted last-rendered values under two global `localStorage` keys (`scp.cost.lastEstimate` and `scp.cost.lastActual`). When user A logged out and user B logged in on the same browser, `restorePersistedCostStrip` read those global keys on B's first paint and showed A's Estimated max and Actual cost. Same story across tabs and browser restarts.
Three layers, so this can't recur
(1) Keys are now per-user.
`lastEstimateKey()` and `lastActualKey()` return `scp.cost.lastEstimate:` and `scp.cost.lastActual:`. Every reader/writer was switched: `renderEstimate`, `renderActualCost`, `hydrateCostStripFromJobs`, `restorePersistedCostStrip`, `clearPersistedCostStrip`.
(2) Logout wipes everything.
`doLogout` now calls `clearAllPersistedCostStrip()` which scans `localStorage` and removes every key starting with `scp.cost.lastEstimate:` or `scp.cost.lastActual:`, plus the legacy un-namespaced keys an older build might have left behind. It also resets the on-screen strip cells and clears in-memory state (`pricingRates`, `activePollJobId`, `lastInvokeAt`) so nothing carries over visually.
(3) Defense at boot.
`bootstrapPrincipal` records the principal name in `scp.cost.lastPrincipal`. On every boot, if the just-fetched principal differs from what was last seen on this browser, every cost bucket is wiped before `restorePersistedCostStrip` has a chance to read. Catches the case where a previous logout didn't run (tab closed, JWT manually nuked, etc).
Test plan