You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Report a vulnerability : Editor overlay renders raw input as HTML when the highlight overlay has no .line spans #184
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)returnto;
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';constout=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.
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.
I will fix this. The branch that returns the plain text needs to escape it before the overlay parses it as HTML; the fix will ship with the repro as a unit test.
Thanks for the quick turnaround on this, and for the release.
One thing I noticed while upgrading to 4.11.4: the shipped code doesn't seem to match the PR #185 description. The description says the fallback now escapes &, < and >, but in the 4.11.4 package:
dist/internal/speculative.js is byte-identical to 4.11.3 and still has if (lines.length === 0) return to;, so the raw input is still returned unescaped.
The only change I can see is in dist/internal/components/Input.svelte, which now runs carta.sanitizer(html) over the speculative result, and only when a sanitizer is configured.
So the fix depends on the sanitizer rather than escaping, which leaves two gaps:
No sanitizer configured: the overlay still renders typed markup as HTML, the same as 4.11.3.
Permissive sanitizer: any tag the sanitizer allows still renders live in the overlay. For example, a sanitizer that allows <img src="https://..."> turns typed text like <img src=https://example.com/x.png> into a real remote request while the user is typing. Tags the sanitizer strips also make the overlay text drift from the textarea text, so the highlight no longer lines up.
Escaping in the fallback, as the PR text describes, would cover both cases, since the overlay only needs to show the text, not render it:
Activity
itsmunzir commentedon Oct 4, 2026
I will fix this. The branch that returns the plain text needs to escape it before the overlay parses it as HTML; the fix will ship with the repro as a unit test.
fix(#184): escape the overlay's plain-text fallback
github-actions commentedon Oct 5, 2026
🎉 This issue has been resolved in version carta-md-v4.11.4 🎉
The release is available on:
Your semantic-release bot 📦🚀
imunisasi commentedon Oct 6, 2026
Thanks for the quick turnaround on this, and for the release.
One thing I noticed while upgrading to 4.11.4: the shipped code doesn't seem to match the PR #185 description. The description says the fallback now escapes
&,<and>, but in the 4.11.4 package:dist/internal/speculative.jsis byte-identical to 4.11.3 and still hasif (lines.length === 0) return to;, so the raw input is still returned unescaped.dist/internal/components/Input.svelte, which now runscarta.sanitizer(html)over the speculative result, and only when a sanitizer is configured.So the fix depends on the sanitizer rather than escaping, which leaves two gaps:
<img src="https://...">turns typed text like<img src=https://example.com/x.png>into a real remote request while the user is typing. Tags the sanitizer strips also make the overlay text drift from the textarea text, so the highlight no longer lines up.Escaping in the fallback, as the PR text describes, would cover both cases, since the overlay only needs to show the text, not render it:
BearToCode commentedon Oct 7, 2026
Sorry, you are right. This fixes the vulnerability but the behavior is still wrong. I will just escape everything as you suggest.
fix(#184): escape HTML on first speculative update
github-actions commentedon Oct 7, 2026
🎉 This issue has been resolved in version carta-md-v4.11.5 🎉
The release is available on:
Your semantic-release bot 📦🚀