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(/