Summary
gomarkdown/markdown IsSafeURL panics on short URLs
Advisory details
Summary
IsSafeURL slices url[:nPath] before checking that len(url) >= nPath, so a URL shorter than a path prefix (0, 1 or 2 bytes) triggers a slice-bounds panic, and this is reached end to end when the html.Safelink hardening flag is enabled and the input contains an empty or very short link destination, giving an unrecoverable crash DoS. Confirmed at HEAD (e08cff8; present since 2022).
The defect
parser/parser.go, IsSafeURL:
func IsSafeURL(url []byte) bool {
nLink := len(url)
for _, path := range validPaths { // "/", "./", "../"
nPath := len(path)
linkPrefix := url[:nPath] // line 789: sliced BEFORE the length guard
if nLink >= nPath && bytes.Equal(linkPrefix, path) { // line 790: guard is too late
...
url[:nPath] is evaluated before the nLink >= nPath check. When len(url) < nPath and cap(url) < nPath, the slice expression panics. Only the relative-path loop is affected; the adjacent URIs loop is already correctly guarded (the signal that this one deviates, the same signal as the anchor). Input: IsSafeURL("") panics ([:1] cap 0), IsSafeURL(".") panics ([:2] cap 1), IsSafeURL("..") panics ([:3] cap 2), while IsSafeURL("/") returns true. End to end: markdown.ToHTML(input, html.CommonFlags|html.Safelink) with input [x]() panics. Attacker model: (1) any service rendering untrusted Markdown with the html.Safelink flag (the URL-sanitizing hardening flag itself introduces the crash); or (2) any downstream that calls the exported parser.IsSafeURL on a 0/1/2-byte attacker URL, no flag required. No recover() anywhere, so an uncaught crash of the calling goroutine.
Verification
EXECUTED with negative controls. Direct: the empty/1/2-char cases panic with the exact slice-bounds message while "/" returns true. End to end via ToHTML with html.CommonFlags|html.Safelink: [x]() panics, while [x](/), [x](http://example.com), plain text, and [x](javascript:alert(1)) (safely dropped) do not. NOT verified: how many downstream apps enable Safelink or call IsSafeURL directly (the default no-Safelink render path does not reach it).
Suggested fix
Move the slice inside the guard: check nLink >= nPath first, then compute url[:nPath] (or use bytes.HasPrefix(url, path), which is length-safe). One-line change, mirroring the already-correct URIs loop.
Also checked and sound: CVE-2026-40890 (smartypants smartLeftAngle OOB) is fixed; CVE-2024-44337 (list/paragraph infinite loop) is fixed (deep nesting capped at maxNesting=64); default XSS is by design (Safelink/SkipHTML not in CommonFlags).
Tooling
AI assistance was used while investigating. The panic is executed-verified with negative controls (IsSafeURL on empty/./.. panics with a slice-bounds error while "/" returns true; ToHTML with html.Safelink on [x]() panics while valid and javascript: links do not), and I confirmed at HEAD that IsSafeURL computes url[:nPath] before the nLink >= nPath guard.
References
Related vulnerabilities
All Supply chain →- HIGHCVE-2026-96232
Microsoft Security Advisory CVE-2026-96232 - .NET and Visual Studio Information Disclosure Vulnerability
- HIGHCVE-2026-107302
msgpack5: Truncated map32 headers throw an unexpected error
- MEDIUMCVE-2026-107220
Excelize: Panic in cellInRange on a worksheet with an empty mergeCell ref
- MEDIUMCVE-2026-102820
pageant: Out-of-bounds read / oversized allocation in `pageant` MemoryMap::read via a malicious Pageant agent (Windows)
- CRITICALGHSA-26w7-cxv4-gfx2
Astro: Remote code execution through AVIF image optimization
- HIGHGHSA-6v7p-g79w-8964
MessagePack for Python (the `msgpack` package) has an out-of-bounds read in versions up to and including 1.2.0. If an `Unpacker` is reused after it has raised and caught an error, it can read out of bounds and crash the process with a segmentation fault. Code that streams untrusted MessagePack through a single long-lived `Unpacker` can therefore be crashed on demand, a denial-of-service risk.