Repository navigation
Stop document content from injecting CSS through Highlight and link marks #399
Copy link
Copy link
Closed
Labels
EditorTiptap & ProsemirrorTiptap & ProsemirrorExtensionSecuritySecurity, access control, and data exposureSecurity, access control, and data exposurebugSomething isn't workingSomething isn't working
Milestone
Description
Summary
Two editor attributes copy raw strings from document content into the DOM:
- The Highlight
colorgoes into astyleattribute. - The hyperlink
classcomes from pasted HTML.
Anyone who can edit a public document can add a full-window overlay that every reader sees. This is CSS only. It cannot run script and cannot read typed passwords. It can cover the page with fake UI and a link to an outside site.
- Severity: Medium
- Area:
apps/webappTipTap,extensions/extension-hyperlink(published package) - Source: security review of 2026-10-06. Proven against the locked
@tiptap/core3.31.3 andprosemirror-model1.25.11.
Where
apps/webapp/src/components/TipTap/extensions/highlight.ts:22-29parseHTMLkeeps the rawdata-colororstyle.backgroundColor.renderHTMLwritesstyle: \background-color: ${attributes.color}`. ProseMirror callssetAttribute('style', …)`, so every declaration in the string applies.
- Highlight is registered in the document editor at
apps/webapp/src/components/TipTap/TipTap.tsx:224. The History view uses the same setup. extensions/extension-hyperlink/src/hyperlink.ts:228-230— theclassattribute has noparseHTML, so Tiptap's default readselement.getAttribute('class').:266merges it into the rendered<a>. Compiled Tailwind classes such asfixed inset-0 z-[60]then apply.- The peer-caret code already guards this case (
TipTap.tsx:335-336). Highlight does not.
Who can do it
Any signed-out visitor on a public document that is not Read-only. Read-only is off by default. A raw Yjs write skips parseHTML, so the render side must check too.
Fix plan
- Highlight
color:- Accept only a value that matches a strict pattern, for example
^#[0-9a-fA-F]{3,8}$,rgb()/rgba()with numbers only, or a name from the editor palette. - Apply the check in both
parseHTMLandrenderHTML. Stored marks and raw Yjs updates skip the parser. - On an invalid value, render no
styleand nodata-color.
- Accept only a value that matches a strict pattern, for example
- Hyperlink
class:- Set
parseHTML: () => nullandrendered: falseon theclassattribute inextensions/extension-hyperlink/src/hyperlink.ts. - Keep
this.options.HTMLAttributes.classfrom extension options, which is developer-set and safe. - This is a published package: add a CHANGELOG line and follow the
release-extensionsskill.
- Set
- Optional hardening: check other attributes that render into
style.TextAlignwrites an uncheckedstyle, but it is registered with notypestoday, so it is dead.
Acceptance criteria
- Pasting
<mark data-color="#ff0;position:fixed;inset:0">x</mark>renders a mark with nostyleattribute. - A stored mark with a bad
color(raw Yjs) renders with nostyle. - Normal highlight colours from the toolbar still render.
- Pasting
<a href="/api/browser/proxy?url=https%3A%2F%2Fexample.com" class="fixed inset-0">x</a>renders a link with no attacker class. - Hyperlink round-trip specs still pass (
extensions/extension-hyperlink).
Verify
Add a unit spec for the colour validator, and a Cypress paste spec in the playground harness (see extensions/CLAUDE.md §round-trip specs).
Generated by Claude Code
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
EditorTiptap & ProsemirrorTiptap & ProsemirrorExtensionSecuritySecurity, access control, and data exposureSecurity, access control, and data exposurebugSomething isn't workingSomething isn't working
Activity
fix(editor): stop document content from injecting CSS
docs: record the security and push fixes in changelogs and agent docs
[-][Security] Document content can inject CSS through the Highlight colour and the link class[/-][+]Stop document content from injecting CSS through Highlight and link marks[/+]HMarzban commentedon Oct 6, 2026
Status: fixed in
b65b33eonclaude/youthful-lovelace-3nvsc6. Not merged or deployed yet.What shipped:
apps/webapp/src/components/TipTap/extensions/highlight.ts:safeColoraccepts only a hex value (3, 4, 6 or 8 digits) or a numericrgb()orrgba(). It runs inparseHTMLand inrenderHTML. A bad value renders nostyleand nodata-color.extensions/extension-hyperlink/src/hyperlink.ts: theclassattribute hasparseHTML: () => nullandrendered: false. OnlyHTMLAttributes.classfrom the extension options reaches the<a>.### Securityline inextensions/extension-hyperlink/CHANGELOG.md, a rule in itsAGENTS.md, and a Cypress case inextensions/extension-hyperlink/cypress/e2e/xss-guards.cy.ts.Differences from the plan:
yellowis now dropped, and the mark renders in its default colour.apps/webapp/cypress/e2e/editor/formatting/highlight.cy.jsstill covers toolbar highlights.@docs.plus/extension-hyperlinkis not cut. The webapp usesworkspace:*, so it gets the fix on deploy. Publish later through therelease-extensionsskill.Check after deploy: paste a highlight with an invalid colour and a link with a class. Neither may add a
styleor a class in the rendered page.Generated by Claude Code
chore(merge): land the security and push fixes branch
fix: close follow-ups from the security and push review