Feat: detect account-scoped storage written without an owner discriminator - #214
Merged
Conversation
ivory-code
added a commit
that referenced
this pull request
Aug 22, 2026
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.
Summary
Client-side persistence written for account-scoped data without an owner discriminator survives account switches: the second account inherits the first account's drafts, acceptance flags, or cached entitlements. This class of defect passes single-account QA and only surfaces on account switch or session expiry, so per-file review rarely catches it. QAMap now promotes such writes to risk evidence and plans the account-switch scenario.
Behavioral Contract
When a diff adds a
localStorage/sessionStoragewrite in a product module whose full text references an authenticated user (session hook, user id, auth token), and neither the key expression, the resolved key literal, nor the surrounding write statement carries an owner discriminator, QAMap emits diff risk evidence (account-scoped-storage:<storage>) anchored to the write line and adds the QA scenario "Account switch inherits storage without an owner discriminator" (account switch, session expiry then re-login, second tab).Writes stay unflagged when the key or nearby payload carries an owner discriminator (
ownerId,userId, template interpolation over user/account identifiers), when the key names a device-scoped value (theme, locale, dismissed banner), or when the module has no authenticated-user reference. Detection reads the changed file's head text through the same path as runtime-activation tracing; unified-0 diffs alone cannot see the surrounding auth context.Evidence
Closes #212.
test/benchmarks/web-account-scoped-storage— a terms page using a session hook gainslocalStorage.setItem("terms:accepted-at:v1", ...)with no owner scope; contract pins the intent, reached file, and the new scenario title.startLineto the added write.sessionStorageunder auseUserhook (relaxed value expression —JSON.stringify(...)payloads match).{ ownerId: userId, ... }), device-scopedapp:themekey inside an authenticated module, and an anonymous visit counter with no auth reference — none flagged, scenario absent.Checks
pnpm testpnpm bench:cifor inference, routing, trace, or outputpnpm bench:executionfor E2E compiler or execution fixturespnpm scanfor scanner, security, or repository policypnpm plugin:checkandpnpm plugin:smokefor plugin changesPublic OSS Check
Review Notes
pnpm scan(0 findings),pnpm bench:context(10/10),pnpm plugin:check, andpnpm plugin:smokealso pass locally; they are marked N/A above because this change touches none of those surfaces. The detector is intentionally conservative: string-literal and single-identifier keys only (identifier keys resolve through a same-module literal assignment), module-level auth signals limited to explicit hooks/identifiers/tokens, and the existing generic "Scoped persisted context isolation and cleanup" scenario still fires alongside the new one — the two answer different questions (cleanup semantics vs cross-account inheritance). Dynamic keys built outside the module and auth context imported behind aliases remain review work.