Prevent drop malicious content on the editor#3319
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a security guardrail for drag-and-drop into the editor by inspecting dropped text/html and attempting to block drops that appear malicious (e.g., forbidden elements or content altered by sanitization).
Changes:
- Introduces new
DropOptionsonEditorOptionsto configure forbidden elements and whether to prevent potentially malicious dropped HTML. - Updates
DOMEventPlugindrop handling to inspect/sanitize dropped HTML and prevent the drop under certain conditions. - Adds/updates unit tests covering drop-malicious-content prevention scenarios.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| packages/roosterjs-content-model-types/lib/index.ts | Exports the newly added DropOptions type. |
| packages/roosterjs-content-model-types/lib/editor/EditorOptions.ts | Adds DropOptions and includes it in EditorOptions. |
| packages/roosterjs-content-model-core/lib/corePlugin/domEvent/DOMEventPlugin.ts | Adds drop-time HTML inspection + prevention logic based on sanitization/forbidden elements. |
| packages/roosterjs-content-model-core/test/corePlugin/domEvent/DomEventPluginTest.ts | Adds tests for the new drop prevention behavior and updates existing drop test to pass an event. |
Comments suppressed due to low confidence (1)
packages/roosterjs-content-model-core/lib/corePlugin/domEvent/DOMEventPlugin.ts:164
- Even when the drop is blocked via
preventDefault()/stopPropagation(), the plugin still takes an undo snapshot and firescontentChangedwithChangeSource.Drop. That can trigger downstream drop handlers (e.g., ImageEditPlugin) even though no DOM change occurred. Consider skipping the snapshot/event whene.defaultPreventedis true (or returning early after blocking).
if (this.editor) {
this.editor.takeSnapshot();
this.editor.triggerEvent('contentChanged', {
source: ChangeSource.Drop,
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/roosterjs-content-model-core/lib/corePlugin/domEvent/DOMEventPlugin.ts
Outdated
Show resolved
Hide resolved
| const sanitizedHTML = this.editor?.getDOMCreator().htmlToDOM(html).body.innerHTML; | ||
| if ( | ||
| sanitizedHTML !== html || | ||
| this.forbiddenDropElement.some(element => html.includes(element)) | ||
| ) { |
There was a problem hiding this comment.
this.forbiddenDropElement.some(element => html.includes(element)) is a fragile security check: it is case-sensitive and can false-positive on text/attributes (e.g., "giftiframe") and miss <IFRAME> variants. Prefer parsing to DOM and checking actual tag presence (e.g., querySelector for the forbidden tags, case-insensitive/boundary-safe).
| const sanitizedHTML = this.editor?.getDOMCreator().htmlToDOM(html).body.innerHTML; | |
| if ( | |
| sanitizedHTML !== html || | |
| this.forbiddenDropElement.some(element => html.includes(element)) | |
| ) { | |
| const parsedHtml = this.editor?.getDOMCreator().htmlToDOM(html); | |
| const sanitizedHTML = parsedHtml?.body.innerHTML; | |
| const hasForbiddenElement = !!parsedHtml?.body.querySelector( | |
| this.forbiddenDropElement.join(',') | |
| ); | |
| if (sanitizedHTML !== html || hasForbiddenElement) { |
packages/roosterjs-content-model-types/lib/editor/EditorOptions.ts
Outdated
Show resolved
Hide resolved
packages/roosterjs-content-model-core/test/corePlugin/domEvent/DomEventPluginTest.ts
Outdated
Show resolved
Hide resolved
| const doc = this.editor.getDocument(); | ||
| const html = e.dataTransfer?.getData('text/html'); | ||
|
|
||
| if (html && this.forbiddenDropElement.length > 0) { |
There was a problem hiding this comment.
I think we should still allow the drop to proceed, but set the HTML data transfer with the sanitized content from htmlToDom. The trustedHTMLHandler should already strip the <script> tag from the content. And to be extra safe, we could use querySelector to remove all <iframe> elements from the content before inserting it.
With the current approach, if a user drags and drops HTML that contains both legitimate content and an <iframe>, the entire drop becomes a no-op, we'd lose the non-malicious HTML as well.
Introduced protection to prevent potentially harmful HTML content from being added to the editor through drag-and-drop. The DragAndDrop Plugin was implemented to manage external content drops, block the default drop action, sanitize any dropped content, and insert only the sanitized content into the editor.