fix(markdown): drop a script-bearing href from Markdown links

Merged
MH
14d ago
+53-3

Summary

DocsUI::Markdown#node_link rendered whatever URL Commonmarker handed it. Phlex refuses a javascript: href on its own, but data:, vbscript: and the rest went straight into the page. The link handler now uses the same scheme guard the image handler got in #99: an href is kept for http, https, mailto, tel or no scheme at all (relative, root-relative, protocol-relative, #anchor); any other scheme renders the link text with no <a>. The scheme is read after stripping ASCII whitespace and control characters, as browsers do, so java<TAB>script: can't slip through.

Found while implementing #94 (noted in #99's deviation log).

Test plan

  • spec/docs_ui/markdown_spec.rb "link schemes": a javascript: href keeps its text and loses the <a>; data:, a tab-obfuscated scheme and an upper-case scheme are dropped; http(s), mailto:, tel: and every scheme-less form are kept verbatim
  • bundle exec rspec 1119 examples, 0 failures (93.0% line coverage)
  • bundle exec rubocop clean

Deviations & judgment calls

  • LINK_SCHEMES and link_href live in DocsUI::MarkdownImages next to URL_SCHEME and image_src, not in markdown.rb: that class sits at RuboCop's 100-line limit, and the two guards share the whitespace-stripping scheme read.
  • Blocked links keep their text (an image with a blocked src is dropped entirely): losing prose mid-sentence would be worse than losing a link.
  • mailto: and tel: are allowed for links; images stay http(s)-only.

Summary by cubic

Fixes a Markdown injection vector where script-bearing link hrefs could make it into the page.

[text](javascript:…) previously rendered a live <a>; data:, vbscript:, and obfuscated schemes slipped through while only javascript: was blocked by Phlex. Links now keep their href only for http, https, mailto, tel, or no scheme at all. Blocked links render their text without the anchor; scheme-less hrefs (relative, root-relative, protocol-relative, #anchor) are unchanged.

  • Applies the same scheme guard the image handler already had.
  • Reads the scheme after stripping ASCII whitespace and control characters, matching browser behavior.
  • Adds tests covering allowed, blocked, and obfuscated schemes.