Markdown: parser's isSafeUrl documents rejecting data: but only rejects data:text/html
Summary
@astryxdesign/core/Markdown applies two different URL checks at two stages, and the weaker one guards the public parser API.
| Stage | Function | Rejects |
|---|---|---|
Parser (parser.js) |
isSafeUrl |
javascript:, vbscript:, data:text/html |
Renderer (Markdown.js) |
sanitizeUrl |
/^(javascript|data|vbscript):/i |
The renderer is the stricter of the two and blocks all data:. The <Markdown> component itself is not affected — it never emits a data: href or src, and on rejection it falls back to plain text without calling the components override.
The issue is the parser, which is a public export.
Doc/implementation mismatch
isSafeUrl's doc comment reads:
Reject URLs with dangerous schemes (javascript:, vbscript:, data:) that could execute arbitrary code when rendered as link hrefs or image srcs.
It names data:, but the implementation tests only data:text/html.
Why it matters
parseMarkdown, parseMarkdownIncremental, parseInline and createIncrementalState are public exports of @astryxdesign/core/Markdown. A consumer building a custom renderer over the AST receives data: URLs in link.href and image.src, and would reasonably read that doc comment as meaning the scheme check had already been applied.
Reproduction (0.5.4)
import { parseMarkdown } from '@astryxdesign/core/Markdown'
parseMarkdown('[x](data:image/svg+xml,<svg onload=alert(1)>)')
// → link node, href: "data:image/svg+xml,<svg onload=alert(1)>"
parseMarkdown('')
// → image node, src: "data:image/png;base64,AAAA"For contrast, javascript:, vbscript: and data:text/html are all correctly left as literal text by the same function.
Suggested fix
Have both stages share one helper, or align isSafeUrl with DANGEROUS_URL_PATTERN. Sharing is preferable: the two currently disagree and nothing keeps them in step.
Secondary, lower confidence: a denylist has to anticipate every dangerous scheme. isSafeUrl also passes file:, blob:, about: and protocol-relative //host/path. None of those is script execution, but an allowlist (http:, https:, mailto:, fragments) would be more robust for a control described as rejecting "dangerous schemes".
Version
Verified against 0.5.4. I could not check 0.6.x — our registry mirror has not picked it up yet — so this may already be fixed on main.
Source: facebook/astryx