#6323·astryx

Markdown: parser's isSafeUrl documents rejecting data: but only rejects data:text/html

Author: kevin-walorskiCreated Sep 14, 2026Updated Sep 14, 2026

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)

javascript
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('![x](data:image/png;base64,AAAA)')
// → 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.