Skip to content

Report a vulnerability : Editor overlay renders raw input as HTML when the highlight overlay has no .line spans #184

Closed

Description

@imunisasi

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:

  1. 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.
  2. 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('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');

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.

Activity

  1. itsmunzir commented on Oct 4, 2026

    @itsmunzir
    Contributor

    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.

  2. added a commit that references this issue on Oct 5, 2026
  3. github-actions commented on Oct 5, 2026

    @github-actions

    🎉 This issue has been resolved in version carta-md-v4.11.4 🎉

    The release is available on:

    Your semantic-release bot 📦🚀

  4. imunisasi commented on Oct 6, 2026

    @imunisasi
    Author

    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:

    1. No sanitizer configured: the overlay still renders typed markup as HTML, the same as 4.11.3.
    2. 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:

    if (lines.length === 0)
    	return to.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
  5. BearToCode commented on Oct 7, 2026

    @BearToCode
    Owner

    Sorry, you are right. This fixes the vulnerability but the behavior is still wrong. I will just escape everything as you suggest.

  6. added a commit that references this issue on Oct 7, 2026
  7. github-actions commented on Oct 7, 2026

    @github-actions

    🎉 This issue has been resolved in version carta-md-v4.11.5 🎉

    The release is available on:

    Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions