{"id":"CVE-2026-102941","aliases":["DEBIAN-CVE-2026-102941","EUVD-2026-102934","GHSA-cv23-7vc5-jfh7","UBUNTU-CVE-2026-102941"],"status":"published","title":"gomarkdown/markdown IsSafeURL panics on short URLs","description":"The package github.com/gomarkdown/markdown is a Go library for parsing Markdown text and rendering as HTML. Prior to 0.0.0-20260724235538-61af6e1e6971, parser.IsSafeURL in parser/parser.go slices url[:nPath] before the len(url) \u003e= nPath guard. A short destination causes a slice-bounds panic when the loop reaches a relative-path prefix for which len(url) \u003c nPath and cap(url) \u003c nPath. The one-byte destination / is an explicit non-panic counterexample because it matches the first valid path and returns before a longer prefix is sliced. Demonstrated panic cases include empty, ., and .. destinations under those conditions. The panic is reachable when markdown.ToHTML renders untrusted Markdown with html.Safelink enabled, including an empty link destination, and is also reachable by callers that pass a short attacker-controlled URL directly to the exported parser.IsSafeURL function. An uncaught panic can terminate the calling process and cause denial of service. This issue is fixed in 0.0.0-20260724235538-61af6e1e6971.","severity":{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00279,"percentile":0.20589,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-125"],"published":"2026-10-16T15:38:18Z","modified":"2026-11-11T10:30:37Z","fixes":[{"ecosystem":"Go","package":"github.com/gomarkdown/markdown","fixed_in":["0.0.0-20260724235538-61af6e1e6971"],"sources":["ghsa","osv"]},{"ecosystem":"Debian:14","package":"golang-github-gomarkdown-markdown","fixed_in":["0.0~git20260725.8435af3-1"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"Go","package":"github.com/gomarkdown/markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.0.0-20260724235538-61af6e1e6971"}]}]},{"source":"osv","ecosystem":"Debian:12","package":"golang-github-gomarkdown-markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.0~git20220731.dcdaee8-2","0.0~git20231115.a660076-1","0.0~git20260725.8435af3-1"]},{"source":"osv","ecosystem":"Debian:13","package":"golang-github-gomarkdown-markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.0~git20231115.a660076-1","0.0~git20260725.8435af3-1"]},{"source":"osv","ecosystem":"Debian:14","package":"golang-github-gomarkdown-markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.0~git20260725.8435af3-1"}]}],"versions":["0.0~git20231115.a660076-1"]},{"source":"osv","ecosystem":"Go","package":"github.com/gomarkdown/markdown","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20260724235538-61af6e1e6971"}]}]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"golang-github-gomarkdown-markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.0~git20220731.dcdaee8-2","0.0~git20231115.a660076-1"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"golang-github-gomarkdown-markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.0~git20231115.a660076-1"]}],"products":[{"vendor":"gomarkdown","product":"markdown"}],"references":[{"url":"https://github.com/gomarkdown/markdown/security/advisories/GHSA-cv23-7vc5-jfh7","tags":["x_refsource_CONFIRM","advisory","web","report"]},{"url":"https://github.com/gomarkdown/markdown/commit/61af6e1e69710c5ca13cfbaaaa1bc11bc763a8a9","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/advisories/GHSA-cv23-7vc5-jfh7"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/102xxx/CVE-2026-102941.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-102941","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-102941","tags":["advisory"]},{"url":"https://github.com/gomarkdown/markdown","tags":["package"]},{"url":"https://ubuntu.com/security/CVE-2026-102941","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-102941","tags":["report"]}],"sources":{"cve":[{"id":"CVE-2026-102941","title":"gomarkdown/markdown IsSafeURL panics on short URLs","description":"The package github.com/gomarkdown/markdown is a Go library for parsing Markdown text and rendering as HTML. Prior to 0.0.0-20260724235538-61af6e1e6971, parser.IsSafeURL in parser/parser.go slices url[:nPath] before the len(url) \u003e= nPath guard. A short destination causes a slice-bounds panic when the loop reaches a relative-path prefix for which len(url) \u003c nPath and cap(url) \u003c nPath. The one-byte destination / is an explicit non-panic counterexample because it matches the first valid path and returns before a longer prefix is sliced. Demonstrated panic cases include empty, ., and .. destinations under those conditions. The panic is reachable when markdown.ToHTML renders untrusted Markdown with html.Safelink enabled, including an empty link destination, and is also reachable by callers that pass a short attacker-controlled URL directly to the exported parser.IsSafeURL function. An uncaught panic can terminate the calling process and cause denial of service. This issue is fixed in 0.0.0-20260724235538-61af6e1e6971.","status":"published","published":"2026-10-16T15:38:18.086Z","modified":"2026-10-26T17:37:11.962Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high","assessor":"cna"}],"cwes":["CWE-125"],"updated_at":"2026-11-02T13:39:05.585774Z"}],"euvd":[{"id":"EUVD-2026-102934","description":"The package github.com/gomarkdown/markdown is a Go library for parsing Markdown text and rendering as HTML. Prior to 0.0.0-20260724235538-61af6e1e6971, parser.IsSafeURL in parser/parser.go slices url[:nPath] before the len(url) \u003e= nPath guard. A short destination causes a slice-bounds panic when the loop reaches a relative-path prefix for which len(url) \u003c nPath and cap(url) \u003c nPath. The one-byte destination / is an explicit non-panic counterexample because it matches the first valid path and returns before a longer prefix is sliced. Demonstrated panic cases include empty, ., and .. destinations under those conditions. The panic is reachable when markdown.ToHTML renders untrusted Markdown with html.Safelink enabled, including an empty link destination, and is also reachable by callers that pass a short attacker-controlled URL directly to the exported parser.IsSafeURL function. An uncaught panic can terminate the calling process and cause denial of service. This issue is fixed in 0.0.0-20260724235538-61af6e1e6971.","status":"published","published":"2026-10-16T15:38:18Z","modified":"2026-10-26T17:37:11Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T16:59:28.784227Z"}],"ghsa":[{"id":"GHSA-cv23-7vc5-jfh7","title":"gomarkdown/markdown IsSafeURL panics on short URLs","description":"## Summary\n\nIsSafeURL slices url[:nPath] before checking that len(url) \u003e= 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).\n\n## The defect\n\nparser/parser.go, IsSafeURL:\n\n```go\nfunc IsSafeURL(url []byte) bool {\n    nLink := len(url)\n    for _, path := range validPaths {          // \"/\", \"./\", \"../\"\n        nPath := len(path)\n        linkPrefix := url[:nPath]               // line 789: sliced BEFORE the length guard\n        if nLink \u003e= nPath \u0026\u0026 bytes.Equal(linkPrefix, path) {   // line 790: guard is too late\n            ...\n```\n\nurl[:nPath] is evaluated before the `nLink \u003e= nPath` check. When len(url) \u003c nPath and cap(url) \u003c 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.\n\n## Verification\n\nEXECUTED 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).\n\n## Suggested fix\n\nMove the slice inside the guard: check `nLink \u003e= 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.\n\nAlso 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).\n\n## Tooling\n\nAI 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 \u003e= nPath` guard.","status":"published","published":"2026-10-16T15:41:39Z","modified":"2026-10-16T15:41:44Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"cwes":["CWE-125"],"updated_at":"2026-11-02T14:41:32.483482Z"}],"nvd":[{"id":"CVE-2026-102941","description":"The package github.com/gomarkdown/markdown is a Go library for parsing Markdown text and rendering as HTML. Prior to 0.0.0-20260724235538-61af6e1e6971, parser.IsSafeURL in parser/parser.go slices url[:nPath] before the len(url) \u003e= nPath guard. A short destination causes a slice-bounds panic when the loop reaches a relative-path prefix for which len(url) \u003c nPath and cap(url) \u003c nPath. The one-byte destination / is an explicit non-panic counterexample because it matches the first valid path and returns before a longer prefix is sliced. Demonstrated panic cases include empty, ., and .. destinations under those conditions. The panic is reachable when markdown.ToHTML renders untrusted Markdown with html.Safelink enabled, including an empty link destination, and is also reachable by callers that pass a short attacker-controlled URL directly to the exported parser.IsSafeURL function. An uncaught panic can terminate the calling process and cause denial of service. This issue is fixed in 0.0.0-20260724235538-61af6e1e6971.","status":"published","published":"2026-10-16T16:16:52.937Z","modified":"2026-10-26T18:17:23.42Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high","assessor":"secondary"}],"cwes":["CWE-125"],"updated_at":"2026-11-02T14:13:00.123071Z"}],"osv":[{"id":"CVE-2026-102941","title":"gomarkdown/markdown IsSafeURL panics on short URLs","description":"The package github.com/gomarkdown/markdown is a Go library for parsing Markdown text and rendering as HTML. Prior to 0.0.0-20260724235538-61af6e1e6971, parser.IsSafeURL in parser/parser.go slices url[:nPath] before the len(url) \u003e= nPath guard. A short destination causes a slice-bounds panic when the loop reaches a relative-path prefix for which len(url) \u003c nPath and cap(url) \u003c nPath. The one-byte destination / is an explicit non-panic counterexample because it matches the first valid path and returns before a longer prefix is sliced. Demonstrated panic cases include empty, ., and .. destinations under those conditions. The panic is reachable when markdown.ToHTML renders untrusted Markdown with html.Safelink enabled, including an empty link destination, and is also reachable by callers that pass a short attacker-controlled URL directly to the exported parser.IsSafeURL function. An uncaught panic can terminate the calling process and cause denial of service. This issue is fixed in 0.0.0-20260724235538-61af6e1e6971.","status":"published","published":"2026-10-16T15:38:18.086Z","modified":"2026-11-11T10:30:37.15303956Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"cwes":["CWE-125"],"updated_at":"2026-11-11T11:06:37.779052Z"},{"id":"DEBIAN-CVE-2026-102941","description":"The package github.com/gomarkdown/markdown is a Go library for parsing Markdown text and rendering as HTML. Prior to 0.0.0-20260724235538-61af6e1e6971, parser.IsSafeURL in parser/parser.go slices url[:nPath] before the len(url) \u003e= nPath guard. A short destination causes a slice-bounds panic when the loop reaches a relative-path prefix for which len(url) \u003c nPath and cap(url) \u003c nPath. The one-byte destination / is an explicit non-panic counterexample because it matches the first valid path and returns before a longer prefix is sliced. Demonstrated panic cases include empty, ., and .. destinations under those conditions. The panic is reachable when markdown.ToHTML renders untrusted Markdown with html.Safelink enabled, including an empty link destination, and is also reachable by callers that pass a short attacker-controlled URL directly to the exported parser.IsSafeURL function. An uncaught panic can terminate the calling process and cause denial of service. This issue is fixed in 0.0.0-20260724235538-61af6e1e6971.","status":"published","published":"2026-10-16T16:16:52.937Z","modified":"2026-10-27T09:47:36.50131439Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T15:45:48.903138Z"},{"id":"GHSA-cv23-7vc5-jfh7","title":"gomarkdown/markdown IsSafeURL panics on short URLs","description":"## Summary\n\nIsSafeURL slices url[:nPath] before checking that len(url) \u003e= 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).\n\n## The defect\n\nparser/parser.go, IsSafeURL:\n\n```go\nfunc IsSafeURL(url []byte) bool {\n    nLink := len(url)\n    for _, path := range validPaths {          // \"/\", \"./\", \"../\"\n        nPath := len(path)\n        linkPrefix := url[:nPath]               // line 789: sliced BEFORE the length guard\n        if nLink \u003e= nPath \u0026\u0026 bytes.Equal(linkPrefix, path) {   // line 790: guard is too late\n            ...\n```\n\nurl[:nPath] is evaluated before the `nLink \u003e= nPath` check. When len(url) \u003c nPath and cap(url) \u003c 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.\n\n## Verification\n\nEXECUTED 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).\n\n## Suggested fix\n\nMove the slice inside the guard: check `nLink \u003e= 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.\n\nAlso 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).\n\n## Tooling\n\nAI 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 \u003e= nPath` guard.","status":"published","published":"2026-10-16T15:41:39Z","modified":"2026-10-16T16:00:45.389675499Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"cwes":["CWE-125"],"updated_at":"2026-11-02T15:46:45.639885Z"},{"id":"UBUNTU-CVE-2026-102941","description":"The package github.com/gomarkdown/markdown is a Go library for parsing Markdown text and rendering as HTML. Prior to 0.0.0-20260724235538-61af6e1e6971, parser.IsSafeURL in parser/parser.go slices url[:nPath] before the len(url) \u003e= nPath guard. A short destination causes a slice-bounds panic when the loop reaches a relative-path prefix for which len(url) \u003c nPath and cap(url) \u003c nPath. The one-byte destination / is an explicit non-panic counterexample because it matches the first valid path and returns before a longer prefix is sliced. Demonstrated panic cases include empty, ., and .. destinations under those conditions. The panic is reachable when markdown.ToHTML renders untrusted Markdown with html.Safelink enabled, including an empty link destination, and is also reachable by callers that pass a short attacker-controlled URL directly to the exported parser.IsSafeURL function. An uncaught panic can terminate the calling process and cause denial of service. This issue is fixed in 0.0.0-20260724235538-61af6e1e6971.","status":"published","published":"2026-10-16T16:16:00Z","modified":"2026-10-28T20:15:21.951576322Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T16:55:58.519486Z"}]}}
