You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A pasted node or a channel row can crash a document page for every reader #436
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 mediaUploadPlaceholder node still parses pasted HTML. Any editor can paste it, including a signed-out visitor on a public document that is not Read-only. A filetype value such as constructor makes the icon lookup return Object, and React throws. The change syncs before the author's tab crashes, so every later load crashes too.
A channel row.syncHeadingWidgetUnread puts each channel id into document.querySelector without escaping. A channel id that holds "] throws a SyntaxError inside 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, because join_workspace makes them a member when they open it.
Severity: Medium (availability of a document for all readers)
Called from the useLayoutEffect in apps/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)) in packages/supabase/scripts/13-RLS.sql:125-130. channels.id is varchar(36) with no format check (packages/supabase/scripts/04-channels.sql:5).
Fix plan
MediaUploadPlaceholder.tsx: delete the parseHTML() method. Nothing inserts this node any more, so it must never come from a paste. Stored nodes still load from Yjs, which does not use parseHTML. The server mirror already has none.
MediaUploadPlaceholderBody.tsx, FileTypeIcon: use Object.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 in CSS.escape(channelId). TocTickRail.tsx:281 and messageJumpTiming.ts:36 already use this form.
Layer: the fix stays in the editor node and in the unread service. Do not change useUnreadSync, getChannels, or any SQL.
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 fileType renders 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 typecheck and bun run lint.
Add one case to apps/webapp/cypress/e2e/editor/copy-paste/standard-paste.cy.js. Paste placeholder HTML with an unknown filetype, then assert that no .media-upload-placeholder exists and typing still works. From apps/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 channels row 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 no SyntaxError from headingWidgetUnread.ts.
changed the title [-][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[/+]on Oct 6, 2026
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.
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.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.Production check (2026-10-06, read-only)
^[A-Za-z0-9_-]+$today.channels_member_insertstill accepts any id. Migration20261006130000did not change that policy.Where
apps/webapp/src/components/TipTap/nodes/MediaUploadPlaceholder.tsx:64-66(parseHTML).apps/webapp/src/components/TipTap/nodes/MediaUploadPlaceholderBody.tsx:19(icons[fileType] || icons.imageinFileTypeIcon).parseHTML:apps/hocuspocus.server/src/lib/migration-extensions.ts:29-45.apps/webapp/src/services/headingWidgetUnread.ts:41([data-toc-id="${channelId}"]).useLayoutEffectinapps/webapp/src/hooks/useUnreadSync.ts:17-20.apps/webapp/src/api/channels/getChannels.ts:43-53.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
channels.id.CSS.escapecloses this crash. After Scope heading chat channels to their own document #402, a client can no longer choosechannels.idat all.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
data-type="media-upload-placeholder"element inserts no placeholder node, and the editor keeps working.fileTyperenders the image icon.Verify
bun run --filter @docs.plus/webapp typecheckandbun run lint.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.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
channels.id)Generated by Claude Code