Version: carta-md 4.11.3 (the published dist/ build).
Where
-
dist/internal/speculative.js, lines 24-25, in speculativeHighlightUpdate(from, to, currentHTML):
if (lines.length === 0)
return to;
-
The return value is rendered unescaped by dist/internal/components/Input.svelte, line 260:
{@html displayedOverlay.html}.
What happens
When the current overlay HTML contains no .line elements, the function returns the editor's
plain text to unchanged. The caller treats that value as HTML, so whatever the user typed (or
whatever text the editor was loaded with) is parsed as markup inside the overlay.
Two ordinary situations reach that branch:
- The overlay is empty.
speculativeHighlight reads highlightElem.innerHTML, which is '' until
the first highlighter pass lands, so the first speculative update after mount takes this path.
- A
sanitizer passed to new Carta({ sanitizer }) removes Shiki's <span class="line">
wrappers. Input.svelte runs the configured sanitizer over the highlighter output too, not only
over the rendered preview, so a sanitizer with a tight allow-list (no span, or no class)
leaves the overlay with no .line elements permanently. Every keystroke then goes through the
raw branch.
The configured sanitizer does not protect against this, because the speculative result is not
passed through it.
Minimal repro (any DOM, for example jsdom):
The module is not in the package's exports map, so import it by file path:
import { speculativeHighlightUpdate } from './node_modules/carta-md/dist/internal/speculative.js';
const out = speculativeHighlightUpdate('', '<img src=x onerror=alert(1)>', '');
console.log(out); // '<img src=x onerror=alert(1)>', returned as markup
In the editor: create new Carta({ sanitizer: (html) => DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p'] }) }),
mount MarkdownEditor, and type <img src=x onerror=alert(1)> into the textarea. A live <img>
element appears in the overlay, and its inline handler would run unless a Content-Security-Policy
blocks inline handlers. Under a CSP that blocks scripts, we observed that injected <style>
elements and remote https <img> requests still take effect.
Suggested fix (one line):
if (lines.length === 0)
return to.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
The branch only runs when there are no highlighted lines to patch, so the overlay then shows plain
text either way; escaping changes how it is parsed, not what it says. We carry this change as a
local patch. A unit test calling speculativeHighlightUpdate('', '<img id=x>', '') finds no #x
element in the result with the patch and finds one without it.
Version: carta-md 4.11.3 (the published
dist/build).Where
dist/internal/speculative.js, lines 24-25, inspeculativeHighlightUpdate(from, to, currentHTML):The return value is rendered unescaped by
dist/internal/components/Input.svelte, line 260:{@html displayedOverlay.html}.What happens
When the current overlay HTML contains no
.lineelements, the function returns the editor'splain text
tounchanged. The caller treats that value as HTML, so whatever the user typed (orwhatever text the editor was loaded with) is parsed as markup inside the overlay.
Two ordinary situations reach that branch:
speculativeHighlightreadshighlightElem.innerHTML, which is''untilthe first highlighter pass lands, so the first speculative update after mount takes this path.
sanitizerpassed tonew Carta({ sanitizer })removes Shiki's<span class="line">wrappers.
Input.svelteruns the configured sanitizer over the highlighter output too, not onlyover the rendered preview, so a sanitizer with a tight allow-list (no
span, or noclass)leaves the overlay with no
.lineelements permanently. Every keystroke then goes through theraw branch.
The configured sanitizer does not protect against this, because the speculative result is not
passed through it.
Minimal repro (any DOM, for example jsdom):
The module is not in the package's
exportsmap, so import it by file path:In the editor: create
new Carta({ sanitizer: (html) => DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p'] }) }),mount
MarkdownEditor, and type<img src=x onerror=alert(1)>into the textarea. A live<img>element appears in the overlay, and its inline handler would run unless a Content-Security-Policy
blocks inline handlers. Under a CSP that blocks scripts, we observed that injected
<style>elements and remote
https<img>requests still take effect.Suggested fix (one line):
The branch only runs when there are no highlighted lines to patch, so the overlay then shows plain
text either way; escaping changes how it is parsed, not what it says. We carry this change as a
local patch. A unit test calling
speculativeHighlightUpdate('', '<img id=x>', '')finds no#xelement in the result with the patch and finds one without it.