Skip to content

Keep the access token out of document page props #429

Closed

Description

@HMarzban

Summary

The document page puts the signed-in user's access token in its page props as accessToken. Next.js writes page props into the HTML (__NEXT_DATA__) and into the /_next/data/...json?slugs= response. The service worker caches both in Cache Storage for 24 hours, and sign-out clears no cache. On a shared device, the next person can read the previous user's JWT until it expires, up to 1 hour later.

  • Severity: Low (shared devices only)
  • Area: webapp auth, service worker
  • Source: security review of 2026-10-06, finding L9

Where

  • Props: apps/webapp/src/utils/documentServerSideProps.ts:77,108 (accessToken: session?.access_token ?? null). The comment at :68-70 covers only the refresh token.
  • Consumers of the prop:
    • apps/webapp/src/pages/[...slugs].tsx:47,87,93. Line 87 uses it only as isAuthed={Boolean(accessToken)} for SlugPageLoader.
    • apps/webapp/src/components/pages/document/DocumentPage.tsx:21,28,44.
    • apps/webapp/src/hooks/useYdocAndProvider.ts:48,55,124. Line 124 falls back to the prop when getSession() returns no session and no error.
  • Cache: next-pwa 5.6.0's others rule (NetworkFirst, maxAgeSeconds: 86400) takes every same-origin GET outside /api/. Its next-data rule needs the URL to end in .json, so the ?slugs= query skips it.
  • Guard: apps/webapp/config/pwa/workbox-runtime-caching.js:89-96 excludes only requests with a token or authorization header. Page and data requests carry neither.
  • Sign-out: apps/webapp/src/components/settings/hooks/useSignOut.ts clears no cache.

Root cause

The token is in the response body. The cache guard reads only request headers, so it cannot see it.

Fix plan

  1. documentServerSideProps.ts: in the document props, replace accessToken with isAuthed: Boolean(session). In the gate props, delete accessToken; the gate does not read it. Update the comment at :68-70.
  2. pages/[...slugs].tsx: pass isAuthed to SlugPageLoader. Stop passing a token to DocumentPage.
  3. DocumentPage.tsx and useYdocAndProvider.ts: remove the accessToken prop and argument. In the token callback, set accessToken: data.session?.access_token ?? ''. The browser client reads the same auth cookies (createBrowserClient in apps/webapp/src/utils/supabase/component.ts), so getSession() already holds the session.
  4. workbox-runtime-caching.js:89-91: correct the comment. Say the rule checks request headers only, and that page props must never carry a token.

Layer: the change stays in the page props and their consumers. Do not change the Workbox rules, next.config.js or useSignOut.

Out of scope

  • A NetworkOnly rule for page and /_next/data/ requests. It would stop the offline reload of a visited document. Private metadata in Cache Storage is the same class as the IndexedDB document copy. apps/webapp/CLAUDE.md §Offline mirror records that copy as accepted.
  • Clearing Cache Storage on sign-out. With no token in the props, it guards nothing that the IndexedDB copy does not already expose.
  • Chat drafts and old localStorage keys. Drafts are keyed by workspace and room only (apps/webapp/src/db/messageComposerDB.ts:58). Four localStorage keys that nothing reads hold the last document id, slug and Pad title (apps/webapp/src/hooks/useDocumentMetadata.ts:15-19). These are separate outcomes; file them on their own if wanted.

Acceptance criteria

  • Signed in, a document page's __NEXT_DATA__ holds no accessToken and no JWT.
  • The /_next/data/...json?slugs= response for a document holds no accessToken and no JWT.
  • In a production build, after visiting documents signed in, no Cache Storage entry contains accessToken.
  • A signed-in owner opens their Private document, and the editor connects and syncs as them.
  • A signed-in user sees the signed-in slug-page skeleton; a signed-out visitor sees the signed-out one.
  • Reloading a visited document while offline still works as before.

Verify

  • bun run --filter @docs.plus/webapp typecheck and bun run lint.
  • grep -rn "accessToken" apps/webapp/src/pages apps/webapp/src/utils/documentServerSideProps.ts apps/webapp/src/components/pages/document returns nothing.
  • The service worker runs only in a production build (disable: !isProduction || isE2E in apps/webapp/next.config.js:23). Run bun run --filter @docs.plus/webapp build, then start.
  • Sign in and open two documents, one through a client-side link. In DevTools, open Application, then Cache Storage, then others. No entry contains accessToken.

Related


Generated by Claude Code

Activity

  1. added theissue type on Oct 6, 2026
  2. changed the title [-][Security] The service worker caches the access token and Private metadata after sign-out[/-] [+]Keep the access token out of document page props[/+] on Oct 6, 2026
  3. HMarzban commented on Oct 9, 2026

    @HMarzban
    CollaboratorAuthor

    Fixed and live.

    • Commits: bdce7bd5b
    • Deployed in bbdaae66c (production run 37976785981, green).
    • Document page props carry isAuthed, never the access token.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    AuthOffline-ModeSecuritySecurity, access control, and data exposurebugSomething isn't working

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions