Skip to content

Observation: MetadataProvider's seed principal collapses to @anon in the window where the bearer is absent, so two principals can share one seed scope #5746

Description

@os-zhuang

Recorded while implementing #5664 (PR #5744). Static reading, not reproduced in a browser — filed so triage can decide whether it is worth measuring, not as a claimed defect.

The reading

packages/app-shell/src/providers/MetadataProvider.tsx:

function principalScope(): string {
let token: string | null = null;
try { token = TokenStorage.get(); } catch { /* SSR / storage unavailable */ }
return token ? fingerprintToken(token) : ANON_PRINCIPAL; // '@anon'
}

principalScope() is the half of the seed key that makes a objectui:metadata: entry
unreadable by a different principal — the property pinned by
MetadataProvider.crossPrincipalSeed.test.tsx as "is unreadable by the next principal even
when the purge never ran" (#5198). When TokenStorage.get() answers null that
discrimination collapses: every such session computes the same @anon scope, so one
session's permission-filtered app list is readable by the next.

When the bearer can be absent

createAuthClient.getSession() writes TokenStorage from payload.session.token on every
boot, so for a returning browser the token is normally present. Two paths that reach
principalScope() with it absent:

  1. MetadataProvider reads the seed synchronously at mount, before getSession() has
    resolved. On a browser whose stale token was just purged by
    apps/console/src/lib/auth-preflight.ts (its documented job) there is nothing to read
    and the mount computes @anon.
  2. A deployment or sign-in path whose session is established by cookie alone, which
    auth-preflight.ts's own header calls out: "Cookie-based sign-in (better-auth sets a
    session cookie) does NOT touch this localStorage key."

What is not claimed

The window is short, and it may be unreachable in practice on every path the console
actually takes — that is the part that needs measuring rather than reasoning. Enumerating
what the app writes across a real sign-in (the method #5664 was worked with) would settle
it in one pass.

Partial mitigation already landed on #5744

PR #5744 makes a change of session user drop the previous user's client state wholesale,
including a sessionStorage sweep. That closes the cross-user half of this on any boot
where the session user actually changes. What it does not close is two sessions sharing
@anon inside a single page-load, or a browser whose session-user pointer never got
written.

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:queuesecurity

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions