Skip to content

id(web): prosemirror_to_markdown emits img src/alt/title unescaped — quotes truncate values and can inject attributes on reparse #873

Closed

Description

@dezren39

Problem

The reverse conversion in pkgs/id/src/web/markdown.rs emits an HTML <img> tag by interpolating the image node's src/alt/title values into double-quoted attributes with no escaping at all (markdown.rs:730-754, the width.is_some() || height.is_some() branch; there is no escape helper anywhere in markdown.rs). Any value containing a " breaks the tag, and the breakage is lossy on the next load:

  • Node {src: "x.png", alt: "a \"quoted\" caption", width: 100} is written to the saved markdown as <img src="x.png" alt="a "quoted" caption" width="100" />. Re-parsing that tag yields alt = "a " — the rest of the caption is silently dropped.
  • Worse, a value can inject attributes: alt = "x\" title=\"injected" is emitted as <img src="x.png" alt="x" title="injected" width="100" />, and the re-parse produces a real title = "injected" the document never had. The same applies to src and title values containing quotes (a crafted src can manufacture alt/width attributes on reparse).

Verified this run (2026-10-25) on main 7ce0d74c with a standalone rustc harness replicating the emission format! code and re-parsing with parse_img_tag's verbatim attr_value helper: both cases above reproduce exactly as shown.

Why it matters

Image node attributes are strings the node model places no restriction on: alt text comes from markdown image syntax, from uploaded-file names (result.name in the web editor), and from collaborating clients' document JSON. Whenever such an image also carries a width or height — exactly the case the HTML-emission branch exists for — saving the document writes a malformed tag, and the next open/save cycle silently truncates the attribute or invents new ones. That is slow-motion document corruption of the same class as #872 (which covers the parse direction of this same tag codec), but a distinct defect with a distinct fix: this one is missing output escaping, not substring matching.

Suggested fix

Escape attribute values on emission: at minimum " → &quot; (and & → &amp; first, so existing entities are not double-decoded), for src, alt, and title. Note the interplay with #872: the parser (parse_img_tag) does not currently decode entities, so emission and parsing should agree on one convention — if emission escapes, parsing should decode &quot;/&amp; in values so the round trip is value-preserving. Coordinate the two fixes so the pair forms a consistent codec rather than two half-codecs.

Acceptance criteria

  • Round-trip pin: an image node with alt = "a \"quoted\" caption" and a width survives ProseMirror → markdown → ProseMirror with the alt value byte-identical.
  • An alt/title/src value containing " title=" (or similar) cannot create or alter other attributes on reparse.
  • Values containing & round-trip without double-escaping or entity corruption (state the chosen convention in a test).
  • Existing image conversion tests in markdown.rs keep passing.

Related: #872 (parse direction of the same <img> attribute codec).
Single-repo: this conversion code exists only in developing-today/code (pkgs/id).

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions