fix: prevent reflected HTML injection via filter URL parameter

TipTap's setContent() parses strings as HTML via DOMParser, allowing
crafted ?filter= URL parameters to inject SVG phishing buttons, anchor
tags, and formatted content into the trusted UI.

Use ProseMirror JSON document format instead of raw strings so the
filter value is always set as a text node, bypassing HTML parsing
entirely.
This commit is contained in:
kolaente
2026-02-25 10:20:48 +01:00
parent 71657fce30
commit a42b4f37bd

View File

@@ -170,9 +170,18 @@ function setEditorContentFromModelValue(newValue: string | undefined) {
// Preserve cursor position before updating content
const currentPosition = editor.value.state.selection.from
editor.value.commands.setContent(content, {
emitUpdate: false,
})
// Use JSON content format instead of a plain string to prevent
// TipTap from parsing the value as HTML (reflected HTML injection
// via the ?filter= URL parameter).
editor.value.commands.setContent(content
? {
type: 'doc',
content: [{
type: 'paragraph',
content: [{type: 'text', text: content}],
}],
}
: '', {emitUpdate: false})
// Restore cursor position after content update
// Ensure position is within the new content bounds
@@ -190,9 +199,15 @@ function updateDateInQuery(newDate: string | Date | null) {
const newText = currentText.replace(currentOldDatepickerValue.value, dateStr)
currentOldDatepickerValue.value = dateStr
editor.value.commands.setContent(newText, {
emitUpdate: false,
})
editor.value.commands.setContent(newText
? {
type: 'doc',
content: [{
type: 'paragraph',
content: [{type: 'text', text: newText}],
}],
}
: '', {emitUpdate: false})
const processed = processContent(newText)
lastEmittedValue = processed
emit('update:modelValue', processed)