Skip to content

Stop document content from injecting CSS through Highlight and link marks #399

Description

@HMarzban

Summary

Two editor attributes copy raw strings from document content into the DOM:

  • The Highlight color goes into a style attribute.
  • The hyperlink class comes 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/webapp TipTap, extensions/extension-hyperlink (published package)
  • Source: security review of 2026-10-06. Proven against the locked @tiptap/core 3.31.3 and prosemirror-model 1.25.11.

Where

  • apps/webapp/src/components/TipTap/extensions/highlight.ts:22-29
    • parseHTML keeps the raw data-color or style.backgroundColor.
    • renderHTML writes style: \background-color: ${attributes.color}`. ProseMirror calls setAttribute('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 — the class attribute has no parseHTML, so Tiptap's default reads element.getAttribute('class'). :266 merges it into the rendered <a>. Compiled Tailwind classes such as fixed 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

  1. 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 parseHTML and renderHTML. Stored marks and raw Yjs updates skip the parser.
    • On an invalid value, render no style and no data-color.
  2. Hyperlink class:
    • Set parseHTML: () => null and rendered: false on the class attribute in extensions/extension-hyperlink/src/hyperlink.ts.
    • Keep this.options.HTMLAttributes.class from extension options, which is developer-set and safe.
    • This is a published package: add a CHANGELOG line and follow the release-extensions skill.
  3. Optional hardening: check other attributes that render into style. TextAlign writes an unchecked style, but it is registered with no types today, so it is dead.

Acceptance criteria

  • Pasting <mark data-color="#ff0;position:fixed;inset:0">x</mark> renders a mark with no style attribute.
  • A stored mark with a bad color (raw Yjs) renders with no style.
  • Normal highlight colours from the toolbar still render.
  • Pasting <a href="https://example.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

Activity

  1. added theissue type on Oct 6, 2026
  2. added 2 commits that reference this issue on Oct 6, 2026
    b65b33e
    7560222
  3. changed the title [-][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[/+] on Oct 6, 2026
  4. added
    EditorTiptap & Prosemirror
    SecuritySecurity, access control, and data exposure
    on Oct 6, 2026
  5. HMarzban commented on Oct 6, 2026

    @HMarzban
    CollaboratorAuthor

    Status: fixed in b65b33e on claude/youthful-lovelace-3nvsc6. Not merged or deployed yet.

    What shipped:

    • apps/webapp/src/components/TipTap/extensions/highlight.ts: safeColor accepts only a hex value (3, 4, 6 or 8 digits) or a numeric rgb() or rgba(). It runs in parseHTML and in renderHTML. A bad value renders no style and no data-color.
    • extensions/extension-hyperlink/src/hyperlink.ts: the class attribute has parseHTML: () => null and rendered: false. Only HTMLAttributes.class from the extension options reaches the <a>.
    • A ### Security line in extensions/extension-hyperlink/CHANGELOG.md, a rule in its AGENTS.md, and a Cypress case in extensions/extension-hyperlink/cypress/e2e/xss-guards.cy.ts.

    Differences from the plan:

    • No palette names are accepted. The webapp has no colour picker for Highlight, so there is no palette. A named CSS colour such as yellow is now dropped, and the mark renders in its default colour.
    • Not done: the colour validator unit spec and the Highlight paste spec from "Verify". The first two acceptance criteria have no automated test. The existing apps/webapp/cypress/e2e/editor/formatting/highlight.cy.js still covers toolbar highlights.
    • Step 3 (TextAlign) was optional and was not done.
    • The npm release of @docs.plus/extension-hyperlink is not cut. The webapp uses workspace:*, so it gets the fix on deploy. Publish later through the release-extensions skill.

    Check after deploy: paste a highlight with an invalid colour and a link with a class. Neither may add a style or a class in the rendered page.


    Generated by Claude Code

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

    EditorTiptap & ProsemirrorExtensionSecuritySecurity, access control, and data exposurebugSomething isn't working

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions