Summary
A new document's slug comes from Math.random(), which is not a cryptographic source. Its output can be predicted. The first signed-in focus or edit on an ownerless document claims ownership (CONTEXT.md §Document access, Open document). So someone who predicts a fresh slug can open it first and own another person's new document.
- Severity: Low
- Area: webapp document creation
- Source: security review of 2026-10-06, finding L5
Where
apps/webapp/src/utils/sanitizeDocumentSlug.ts:9 — (Math.random() + 1).toString(36).substring(2).
apps/webapp/src/proxy.ts:13 — the same expression for the new. host redirect.
apps/webapp/src/proxy.ts:36 also uses Math.random(), for log sampling only. Leave it.
Constraint
Root CLAUDE.md §Settled (Next product APIs) says "Do not edit src/proxy.ts". Changing line 13 needs maintainer approval. If refused, change sanitizeDocumentSlug.ts only and have proxy.ts import that helper in a later approved change.
Fix plan
- In
sanitizeDocumentSlug.ts, build the random slug from crypto.getRandomValues (available in the browser, in Bun and in the Next.js edge runtime). Keep the same alphabet (0-9a-z) and a length of at least 11 characters.
- With approval, make
proxy.ts:13 call the same helper instead of repeating the expression.
Acceptance criteria
Verify
grep -rn "Math.random" apps/webapp/src/utils/sanitizeDocumentSlug.ts apps/webapp/src/proxy.ts shows only the log-sampling line.
- Open
/new twice and check two different slugs of the expected length.
Summary
A new document's slug comes from
Math.random(), which is not a cryptographic source. Its output can be predicted. The first signed-in focus or edit on an ownerless document claims ownership (CONTEXT.md§Document access, Open document). So someone who predicts a fresh slug can open it first and own another person's new document.Where
apps/webapp/src/utils/sanitizeDocumentSlug.ts:9—(Math.random() + 1).toString(36).substring(2).apps/webapp/src/proxy.ts:13— the same expression for thenew.host redirect.apps/webapp/src/proxy.ts:36also usesMath.random(), for log sampling only. Leave it.Constraint
Root
CLAUDE.md§Settled (Next product APIs) says "Do not editsrc/proxy.ts". Changing line 13 needs maintainer approval. If refused, changesanitizeDocumentSlug.tsonly and haveproxy.tsimport that helper in a later approved change.Fix plan
sanitizeDocumentSlug.ts, build the random slug fromcrypto.getRandomValues(available in the browser, in Bun and in the Next.js edge runtime). Keep the same alphabet (0-9a-z) and a length of at least 11 characters.proxy.ts:13call the same helper instead of repeating the expression.Acceptance criteria
Math.random()./newand thenew.host still open a fresh document.Verify
grep -rn "Math.random" apps/webapp/src/utils/sanitizeDocumentSlug.ts apps/webapp/src/proxy.tsshows only the log-sampling line./newtwice and check two different slugs of the expected length.