fix(editor): strip event handler attributes from pasted HTML

Draft
DI
1d ago
+26-1

Stacked on #3840.

Hardens the paste sanitizer flagged by code scanning alert #312 (js/xss).

sanitizePastedHtml skips its attribute allowlist when the pasted HTML contains the editor's node-* class marker, so intra-editor copy/paste keeps class, style and data-*. Anyone can put that marker in HTML they serve, and then event handler attributes went through the sanitizer too:

<div class="node-container" onclick="alert(1)"><img src="..." onerror="alert(2)"></div>

This wasn't exploitable: the sanitized HTML goes through Tiptap's generateJSON, and the schema keeps only declared attributes. Running the payload above through sanitizer → generateJSON → editor → composeReactEmail left no on* attribute in the JSON, the editor HTML or the exported email. But the sanitizer shouldn't rely on the schema for that, so it now strips them itself.

Changes

  • utils/paste-sanitizer.ts: new removeEventHandlerAttributes step that always runs, next to removeForbiddenElements and scrubUnsafeUrlAttributes. No editor extension declares an attribute starting with on, so nothing that round-trips today is affected.
  • .changeset/editor-strip-paste-event-handlers.md: patch for @react-email/editor.

The flagged line itself (DOMParser.parseFromString) builds an inert document where scripts and handlers don't run, so CodeQL may keep reporting it; that part is a false positive.

Testing

  • New test, written first and failing on the base: HTML with the editor marker plus onclick / onMouseOver / onerror comes out with no on* attributes and with its node-* classes kept.
  • Editor suite 541/541 (unit + browser, including the HTML paste integration tests). pnpm build, pnpm typecheck and biome pass.

Summary by cubic

Hardens the paste sanitizer so event handler attributes (onclick, onerror, ...) are stripped from pasted HTML even when it carries the editor's node-* class marker. Previously that marker made the sanitizer skip its attribute allowlist, letting on* attributes through; although Tiptap's schema filtered them later, the sanitizer now removes them directly.

Notes

  • Code scanning may keep flagging the DOMParser.parseFromString line; that finding is a false positive because the parser builds an inert document where scripts and handlers don't run.
  • No editor extension declares an attribute starting with on, so intra-editor copy/paste round-trips are unaffected.