Skip to content

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

Closed
dielduarte wants to merge 1 commit into
fix/ui-resolve-email-path-oncefrom
fix/editor-strip-paste-event-handlers
Closed

dielduarte wants to merge 1 commit into
fix/ui-resolve-email-path-oncefrom
fix/editor-strip-paste-event-handlers

Conversation

@dielduarte

@dielduarte dielduarte commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

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.

Written for commit 2125999. Summary will update on new commits.

View guided diff

@vercel

vercel Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
react-email Ready Ready Preview Oct 29, 2026 2:29pm UTC
react-email-demo Ready Ready Preview Oct 29, 2026 2:29pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2125999

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@react-email/editor Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@react-email/editor@3841

commit: 2125999

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files

Confidence score: 4/5

  • removeEventHandlerAttributes in packages/editor/src/utils/paste-sanitizer.ts misses elements inside <template> content, so event-handler attributes can remain in the returned HTML. Traverse template.content as well.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/editor/src/utils/paste-sanitizer.ts">

<violation number="1" location="packages/editor/src/utils/paste-sanitizer.ts:64">
P3: `removeEventHandlerAttributes` doesn't reach `<template>` content, because `getElementsByTagName` only walks child nodes. `<template><img onerror="...\"></template>` keeps its handler in the returned HTML, so the sanitizer still relies on `generateJSON` to drop it. Recurse into `template.content`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

}

function removeEventHandlerAttributes(root: Element): void {
for (const el of Array.from(root.getElementsByTagName('*'))) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: removeEventHandlerAttributes doesn't reach <template> content, because getElementsByTagName only walks child nodes. <template><img onerror="...\"></template> keeps its handler in the returned HTML, so the sanitizer still relies on generateJSON to drop it. Recurse into template.content.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/editor/src/utils/paste-sanitizer.ts, line 64:

<comment>`removeEventHandlerAttributes` doesn't reach `<template>` content, because `getElementsByTagName` only walks child nodes. `<template><img onerror="...\"></template>` keeps its handler in the returned HTML, so the sanitizer still relies on `generateJSON` to drop it. Recurse into `template.content`.</comment>

<file context>
@@ -59,6 +60,16 @@ function removeForbiddenElements(root: Element): void {
 }
 
+function removeEventHandlerAttributes(root: Element): void {
+  for (const el of Array.from(root.getElementsByTagName('*'))) {
+    for (const attr of Array.from(el.attributes)) {
+      if (attr.name.toLowerCase().startsWith('on')) {
</file context>

@dielduarte

Copy link
Copy Markdown
Contributor Author

Closing: tiptap's schema already drops undeclared attributes, and the alert was dismissed as a false positive.

@dielduarte dielduarte closed this Oct 29, 2026

This branch was successfully deployed

2 active deployments
Preview – react-email-demo — 21259992 Deployed Oct 29, 2026 by vercel[bot]
Preview – react-email — 21259992 Deployed Oct 29, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant