Skip to content

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

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

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