From a49ab079ecc130a1ab2702f6e3fdec73ef8f441b Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sat, 24 Oct 2026 22:13:19 +0200
Subject: [PATCH] fix: sanitize quoted and signature HTML in re-opened drafts
(GHSA-g35q-3gpc-x2rr)
---
.../composer-island-sanitize.test.ts | 64 +++++++++++++++++++
components/email/quoted-html.ts | 7 +-
components/email/signature-block.ts | 5 +-
lib/email-sanitization.ts | 19 ++++++
4 files changed, 90 insertions(+), 5 deletions(-)
create mode 100644 components/email/__tests__/composer-island-sanitize.test.ts
diff --git a/components/email/__tests__/composer-island-sanitize.test.ts b/components/email/__tests__/composer-island-sanitize.test.ts
new file mode 100644
index 000000000..e297be0af
--- /dev/null
+++ b/components/email/__tests__/composer-island-sanitize.test.ts
@@ -0,0 +1,64 @@
+import { afterEach, describe, expect, it } from 'vitest';
+import { Editor } from '@tiptap/core';
+import Document from '@tiptap/extension-document';
+import Paragraph from '@tiptap/extension-paragraph';
+import Text from '@tiptap/extension-text';
+import type { Editor as ReactEditor } from '@tiptap/react';
+
+import { QuotedHtml, serializeEditorContent } from '../quoted-html';
+import { SignatureBlock, SIGNATURE_BLOCK_MARKER } from '../signature-block';
+
+// A re-opened draft hands its stored HTML to the editor as is, and the quote
+// and signature islands copy their inner HTML verbatim into the app document.
+// A draft written by another client, or by another member of a shared
+// mailbox, could carry event handlers that then ran in the webmail origin.
+
+const PAYLOAD = 'l
Quoted original
'; + +function mount(html: string): Editor { + return new Editor({ extensions: [Document, Paragraph, Text, QuotedHtml, SignatureBlock], content: html }); +} + +function islandHtml(editor: Editor, selector: string): string { + const host = editor.view.dom.querySelector(selector) as HTMLElement; + return host.shadowRoot!.innerHTML; +} + +function expectInert(html: string) { + expect(html).not.toMatch(/onerror/i); + expect(html).not.toMatch(/javascript:/i); + expect(html).not.toMatch(/