Repository navigation
A pasted node or a channel row can crash a document page for every reader #436
Copy link
Copy link
Closed
Labels
ChatRelated to chat featuresRelated to chat featuresEditorTiptap & ProsemirrorTiptap & ProsemirrorSecuritySecurity, access control, and data exposureSecurity, access control, and data exposurebugSomething isn't workingSomething isn't working
Milestone
Description
Summary
The webapp has no React error boundary, so one render error unmounts the whole document page. Two inputs that other people control reach such an error. The page stays broken for every reader until the bad data is removed.
- A paste. The legacy
mediaUploadPlaceholdernode still parses pasted HTML. Any editor can paste it, including a signed-out visitor on a public document that is not Read-only. Afiletypevalue such asconstructormakes the icon lookup returnObject, and React throws. The change syncs before the author's tab crashes, so every later load crashes too. - A channel row.
syncHeadingWidgetUnreadputs each channel id intodocument.querySelectorwithout escaping. A channel id that holds"]throws aSyntaxErrorinside a layout effect. Channels load for every reader of the document, signed in or not. Any signed-in user can insert a channel row into a document's workspace, becausejoin_workspacemakes them a member when they open it.
- Severity: Medium (availability of a document for all readers)
- Area: webapp editor node, webapp heading unread widget
- Source: security review of 2026-10-06, finding M2. Both paths confirmed in code.
Production check (2026-10-06, read-only)
- No channel id in production fails
^[A-Za-z0-9_-]+$today. channels_member_insertstill accepts any id. Migration20261006130000did not change that policy.
Where
- Paste path:
apps/webapp/src/components/TipTap/nodes/MediaUploadPlaceholder.tsx:64-66(parseHTML).apps/webapp/src/components/TipTap/nodes/MediaUploadPlaceholderBody.tsx:19(icons[fileType] || icons.imageinFileTypeIcon).- The server mirror of this node has no
parseHTML:apps/hocuspocus.server/src/lib/migration-extensions.ts:29-45.
- Selector path:
apps/webapp/src/services/headingWidgetUnread.ts:41([data-toc-id="${channelId}"]).- Called from the
useLayoutEffectinapps/webapp/src/hooks/useUnreadSync.ts:17-20. - Channels load for every reader through
apps/webapp/src/api/channels/getChannels.ts:43-53.
- Channel insert: policy
channels_member_insert(created_by = auth.uid() AND internal.is_workspace_member(workspace_id)) inpackages/supabase/scripts/13-RLS.sql:125-130.channels.idisvarchar(36)with no format check (packages/supabase/scripts/04-channels.sql:5).
Fix plan
MediaUploadPlaceholder.tsx: delete theparseHTML()method. Nothing inserts this node any more, so it must never come from a paste. Stored nodes still load from Yjs, which does not useparseHTML. The server mirror already has none.MediaUploadPlaceholderBody.tsx,FileTypeIcon: useObject.hasOwn(icons, fileType) ? icons[fileType] : icons.image. This covers a node that arrives through a raw Yjs client, which skips the parser.headingWidgetUnread.ts:41: wrap the id inCSS.escape(channelId).TocTickRail.tsx:281andmessageJumpTiming.ts:36already use this form.
Layer: the fix stays in the editor node and in the unread service. Do not change useUnreadSync, getChannels, or any SQL.
Out of scope
- A format check on
channels.id.CSS.escapecloses this crash. After Scope heading chat channels to their own document #402, a client can no longer choosechannels.idat all. - An error boundary around the editor and the TOC. It is a separate resilience change, and its fallback needs design-system work. File it on its own if wanted.
- Other unescaped selectors (
TocDesktop.tsx:68,toc/dnd/utils.ts:8,toc/hooks/useTocDrag.ts:206,toc/hooks/tocActions.ts:94,TipTap.tsx:81). They run in event handlers or timers, not in render. A bad id throws once there and does not unmount the page.
Acceptance criteria
- Pasting HTML that holds a
data-type="media-upload-placeholder"element inserts no placeholder node, and the editor keeps working. - A stored placeholder node with an unknown
fileTyperenders the image icon. - With a channel row whose id holds a double quote in the document's workspace, the document page loads for signed-in and signed-out readers.
- In that state, the heading chat buttons of other headings still show their unread counts.
- Upload placeholders during an upload, and stored legacy placeholders, still render, and Cancel still removes a stored one.
Verify
bun run --filter @docs.plus/webapp typecheckandbun run lint.- Add one case to
apps/webapp/cypress/e2e/editor/copy-paste/standard-paste.cy.js. Paste placeholder HTML with an unknownfiletype, then assert that no.media-upload-placeholderexists and typing still works. Fromapps/webapp, with the dev stack running:bun run cypress:run --spec cypress/e2e/editor/copy-paste/standard-paste.cy.js. - Manual, selector path: in local Supabase, insert a
channelsrow for the document's workspace whose id holds a double quote. Load the document signed in and signed out. The page renders, and the browser console shows noSyntaxErrorfromheadingWidgetUnread.ts.
Related
- Scope heading chat channels to their own document #402 (per-document channel key; it removes the client's choice of
channels.id)
Generated by Claude Code
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
ChatRelated to chat featuresRelated to chat featuresEditorTiptap & ProsemirrorTiptap & ProsemirrorSecuritySecurity, access control, and data exposureSecurity, access control, and data exposurebugSomething isn't workingSomething isn't working
Activity
[-][Security] One paste or one channel row crashes a document page for every reader[/-][+]A pasted node or a channel row can crash a document page for every reader[/+]fix(webapp): harden chat avatars, pasted nodes and new slugs
docs(repo): record the 9 Oct security and email batch in the changelo…
HMarzban commentedon Oct 9, 2026
Fixed and live.
cda1150e4bbdaae66c(production run 37976785981, green).