Fix potential content XSS by escaping dangerous URLs in links and images

* This issue in question was fixed upstream in https://github.com/yuin/goldmark/releases/tag/v1.8.2
* We do, however, have a copy of the affected functions, used as fallbacks when no render hook are defined for e.g. links and images, so we need to port these fixes to our copy of the affected functions.
This commit is contained in:
Bjørn Erik Pedersen
2026-04-01 13:54:10 +02:00
parent 81a5cdca07
commit 479fe6c654
2 changed files with 41 additions and 6 deletions
@@ -1035,3 +1035,33 @@ foo[^1] and bar[^2]
"<p>foo<sup id=\"hb5cdcabc9e678612fnref:1\"><a href=\"#hb5cdcabc9e678612fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup> and bar<sup id=\"hb5cdcabc9e678612fnref:2\"><a href=\"#hb5cdcabc9e678612fn:2\" class=\"footnote-ref\" role=\"doc-noteref\">2</a></sup></p>\n<div class=\"footnotes\" role=\"doc-endnotes\">\n<hr>\n<ol>\n<li id=\"hb5cdcabc9e678612fn:1\">\n<p>footnote one&#160;<a href=\"#hb5cdcabc9e678612fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\">back</a></p>\n</li>\n<li id=\"hb5cdcabc9e678612fn:2\">\n<p>footnote two&#160;<a href=\"#hb5cdcabc9e678612fnref:2\" class=\"footnote-backref\" role=\"doc-backlink\">back</a></p>\n</li>\n</ol>\n</div>",
)
}
func TestRenderLinkDefaultDangerous(t *testing.T) {
t.Parallel()
/*
Content: <p>Link: <a href="javascript:alert(1)">Click me</a>
AutoLink: <a href="">javascript:alert(1)</a>
Image: <img src="javascript:alert(1)" alt="alt"></p>
*/
files := `
-- content/p1.md --
---
title: "p1"
---
Link: [Click me](&#106;avascript:alert(1))
AutoLink: <javascript:alert(2)>
Image: ![alt](&#106;avascript:alert(3))
-- layouts/all.html --
Content: {{ .Content }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html",
`! alert(1)"`,
`! href="javascript:alert(2)"`,
`! alert(3)"`,
)
}
+11 -6
View File
@@ -230,8 +230,9 @@ func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, nod
}
n := node.(*ast.Image)
_, _ = w.WriteString("<img src=\"")
if r.Unsafe || !html.IsDangerousURL(n.Destination) {
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
dest := util.URLEscape(n.Destination, true)
if r.Unsafe || !html.IsDangerousURL(dest) {
_, _ = w.Write(util.EscapeHTML(dest))
}
_, _ = w.WriteString(`" alt="`)
r.renderTexts(w, source, n)
@@ -375,8 +376,9 @@ func (r *hookedRenderer) renderLinkDefault(w util.BufWriter, source []byte, node
n := node.(*ast.Link)
if entering {
_, _ = w.WriteString("<a href=\"")
if r.Unsafe || !html.IsDangerousURL(n.Destination) {
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
dest := util.URLEscape(n.Destination, true)
if r.Unsafe || !html.IsDangerousURL(dest) {
_, _ = w.Write(util.EscapeHTML(util.EscapeHTML(dest)))
}
_ = w.WriteByte('"')
if n.Title != nil {
@@ -445,12 +447,15 @@ func (r *hookedRenderer) renderAutoLinkDefault(w util.BufWriter, source []byte,
}
_, _ = w.WriteString(`<a href="`)
url := r.autoLinkURL(n, source)
url := util.URLEscape(r.autoLinkURL(n, source), false)
label := n.Label(source)
if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) {
_, _ = w.WriteString("mailto:")
}
_, _ = w.Write(util.EscapeHTML(util.URLEscape(url, false)))
if r.Unsafe || !html.IsDangerousURL(url) {
_, _ = w.Write(util.EscapeHTML(url))
}
if n.Attributes() != nil {
_ = w.WriteByte('"')
html.RenderAttributes(w, n, html.LinkAttributeFilter)