Summary
The a renderer in MarkdownFormatter.tsx accepts any href without running it through isSafeHref. With rehypeRaw enabled in the pipeline, raw HTML anchors embedded in blog markdown bypass react-markdown's built-in urlTransform (which would block javascript: on markdown-parsed links), turning into hast nodes that reach the a renderer with attacker-controlled href. The cta-button branch has the same problem. #950 fixed the footnote branch only.
Location
app/src/components/blog/MarkdownFormatter.tsx:528-599
What goes wrong
a: ({ href, children, className }) => {
...
if (className === 'cta-button') {
return (
<a href={href} target="_blank" rel="noopener noreferrer" ...>
{children}
</a>
);
}
return (
<a id={id} href={href} target={href?.startsWith('#') ? '' : '_blank'} ...>
{footnoteNumber || children}
</a>
);
};
Background:
Proof-of-concept markdown:
<a href="javascript:alert(document.domain)" class="cta-button">Click me</a>
Rendered in-app as a styled teal CTA that fires alert on click.
Suggested fix
Run every href through isSafeHref before rendering, in both branches:
a: ({ href, children, className }) => {
const safeHref = href && isSafeHref(href) ? href : undefined;
...
if (className === 'cta-button') {
return (
<a href={safeHref} target="_blank" rel="noopener noreferrer" ...>
{children}
</a>
);
}
return (
<a id={id} href={safeHref} target={safeHref?.startsWith('#') ? '' : '_blank'} ...>
{footnoteNumber || children}
</a>
);
};
If safeHref is undefined, render plain text instead of an anchor. Apply the same change to website/src/components/blog/MarkdownFormatter.tsx.
Severity
High (security — stored XSS via raw HTML anchors in blog markdown). Requires commit access to blog content, but blog content is author-editable and content-review is not a substitute for rendering-layer sanitization.
Relates to
#949, #950 (partial — footnote-only), #956 (hardening PR). Ties into sibling filing on duplicate-file drift.
Summary
The
arenderer inMarkdownFormatter.tsxaccepts anyhrefwithout running it throughisSafeHref. WithrehypeRawenabled in the pipeline, raw HTML anchors embedded in blog markdown bypass react-markdown's built-inurlTransform(which would blockjavascript:on markdown-parsed links), turning into hast nodes that reach thearenderer with attacker-controlledhref. Thecta-buttonbranch has the same problem. #950 fixed the footnote branch only.Location
app/src/components/blog/MarkdownFormatter.tsx:528-599What goes wrong
Background:
urlTransformthat blocksjavascript:/vbscript:schemes for anchors parsed from markdown syntax ([text](javascript:...)).rehypeRawis in the plugin chain (it is — see Security: stop rendering raw HTML in blog markdown without sanitization #949 and PR Harden OG HTML and markdown rendering #956), raw HTML<a href="javascript:alert(1)">x</a>in blog markdown is reparsed as hast and flows through the customarenderer with the rawhrefintact.urlTransformdoes not intercept it.isSafeHrefalready exists in this file (lines 58-79) and is the correct guard. It is applied to footnote hrefs (after Security: validate footnote inline link href schemes #950) but not to the defaultabranch or thecta-buttonbranch.Proof-of-concept markdown:
Rendered in-app as a styled teal CTA that fires
alerton click.Suggested fix
Run every
hrefthroughisSafeHrefbefore rendering, in both branches:If
safeHrefisundefined, render plain text instead of an anchor. Apply the same change towebsite/src/components/blog/MarkdownFormatter.tsx.Severity
High (security — stored XSS via raw HTML anchors in blog markdown). Requires commit access to blog content, but blog content is author-editable and content-review is not a substitute for rendering-layer sanitization.
Relates to
#949, #950 (partial — footnote-only), #956 (hardening PR). Ties into sibling filing on duplicate-file drift.