From 369c832dcb733f99f0bc635c6ea687892f3d16e6 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Sat, 29 Aug 2026 20:38:56 -0700 Subject: [PATCH] Cancel the default paste when Trix handles a paste as a file paste MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a paste carries a file alongside HTML, Trix inserts the file but left the browser's default paste uncanceled, so the browser also inserted the accompanying clipboard HTML directly into the editor — unsanitized — until the next redraw replaced it. If that redraw is disrupted, the browser-inserted markup survives in the live DOM and is later serialized back into the editor value, which can turn attacker markup into executable content. Every other insertFromPaste branch already calls preventDefault. The file branch now does too, so Trix fully owns the paste and untrusted clipboard HTML never lands in the editor DOM. This also matches the documented intent to prioritize files over HTML on paste, rather than inserting both. --- .../app/assets/javascripts/trix.js | 1 + src/test/system/level_2_input_test.js | 19 +++++++++++++++++++ .../controllers/level_2_input_controller.js | 1 + 3 files changed, 21 insertions(+) diff --git a/action_text-trix/app/assets/javascripts/trix.js b/action_text-trix/app/assets/javascripts/trix.js index 7951d35db..581c11fcd 100644 --- a/action_text-trix/app/assets/javascripts/trix.js +++ b/action_text-trix/app/assets/javascripts/trix.js @@ -13484,6 +13484,7 @@ $\ }; } else if (processableFilePaste(this.event)) { var _this$delegate22; + this.event.preventDefault(); paste.type = "File"; paste.file = dataTransfer.files[0]; (_this$delegate22 = this.delegate) === null || _this$delegate22 === void 0 || _this$delegate22.inputControllerWillPaste(paste); diff --git a/src/test/system/level_2_input_test.js b/src/test/system/level_2_input_test.js index 9dd6de8af..1c6a043ce 100644 --- a/src/test/system/level_2_input_test.js +++ b/src/test/system/level_2_input_test.js @@ -205,6 +205,25 @@ testGroup("Level 2 Input", testOptions, () => { expectDocument(`${OBJECT_REPLACEMENT_CHARACTER}\n`) }) + test("pasting a file alongside HTML cancels the browser's default paste", async () => { + const file = await createFile() + const dataTransfer = createDataTransfer({ + "text/html": "", + "text/plain": "x", + Files: [ file ], + }) + + const inputEvent = createEvent("beforeinput", { inputType: "insertFromPaste", dataTransfer }) + const notPrevented = document.activeElement.dispatchEvent(inputEvent) + await delay(60) + + assert.notOk(notPrevented, "the default paste must be canceled so the browser cannot insert the clipboard HTML") + + const attachments = getDocument().getAttachments() + assert.equal(attachments.length, 1, "the pasted file is inserted as an attachment") + assert.notOk(getEditorElement().value.includes("onerror"), "no attribute from the clipboard HTML survives") + }) + // "insertFromPaste InputEvent missing pasted files in dataTransfer" // - https://bugs.webkit.org/show_bug.cgi?id=194921 test("pasting a file in Safari", async () => { diff --git a/src/trix/controllers/level_2_input_controller.js b/src/trix/controllers/level_2_input_controller.js index 121461d51..81e7e746c 100644 --- a/src/trix/controllers/level_2_input_controller.js +++ b/src/trix/controllers/level_2_input_controller.js @@ -403,6 +403,7 @@ export default class Level2InputController extends InputController { return this.delegate?.inputControllerDidPaste(paste) } } else if (processableFilePaste(this.event)) { + this.event.preventDefault() paste.type = "File" paste.file = dataTransfer.files[0] this.delegate?.inputControllerWillPaste(paste)