Skip to content

Anchor renderer in MarkdownFormatter does not run href through isSafeHref #989

@MaxGhenis

Description

@MaxGhenis

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity-related issue

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions