Marked version: 18.0.9 (also reproduces on every version tested, back to v12)
Markdown flavor: CommonMark
Repro
const { marked } = require('marked');
marked('<https://example.com/?x=1<2>');
Expected (commonmark.js and CommonMark spec example 595 — & in the destination is escaped to &):
<p><a href="https://example.com/?x=1&lt;2">https://example.com/?x=1&lt;2</a></p>
Actual (raw & in the href):
<p><a href="https://example.com/?x=1<2">https://example.com/?x=1<2</a></p>
Because < is a valid HTML character reference, a browser decodes it inside the attribute, so the link actually points to https://example.com/?x=1<2 — a different URL than was written. Confirmed by parsing marked's output with parse5:
marked href, parsed back: "https://example.com/?x=1<2" (wrong)
commonmark href, parsed: "https://example.com/?x=1<2" (correct)
Note marked already escapes the same URL in the anchor text (&) but not in the href, so the output is internally inconsistent. Any URL containing a valid entity sequence (<, &, ©, …) is affected; this is silent link-target corruption for user- or attacker-supplied URLs.
Cause
Renderer.link() writes cleanUrl(href) straight into the attribute without escapeHtmlEntities() — while the title attribute one line below is escaped. cleanUrl only runs encodeURI, which never encodes &, so the raw & reaches the attribute. (Renderer.image() has the same pattern for src.)
Marked version: 18.0.9 (also reproduces on every version tested, back to v12)
Markdown flavor: CommonMark
Repro
Expected (commonmark.js and CommonMark spec example 595 —
&in the destination is escaped to&):Actual (raw
&in the href):Because
<is a valid HTML character reference, a browser decodes it inside the attribute, so the link actually points tohttps://example.com/?x=1<2— a different URL than was written. Confirmed by parsing marked's output with parse5:Note marked already escapes the same URL in the anchor text (
&) but not in thehref, so the output is internally inconsistent. Any URL containing a valid entity sequence (<,&,©, …) is affected; this is silent link-target corruption for user- or attacker-supplied URLs.Cause
Renderer.link()writescleanUrl(href)straight into the attribute withoutescapeHtmlEntities()— while thetitleattribute one line below is escaped.cleanUrlonly runsencodeURI, which never encodes&, so the raw&reaches the attribute. (Renderer.image()has the same pattern forsrc.)