CVE-2024-55601
Description
Hugo is a static site generator. Starting in version 0.123.0 and prior to version 0.139.4, some HTML attributes in Markdown in the internal templates listed below not escaped in internal render hooks. Those whoa re impacted are Hugo users who do not trust their Markdown content files and are using one or more of these templates: _default/_markup/render-link.html from v0.123.0; _default/_markup/render-image.html from v0.123.0; _default/_markup/render-table.html from v0.134.0; and/or shortcodes/youtube.html from v0.125.0. This issue is patched in v0.139.4. As a workaround, one may replace an affected component with user defined templates or disable the internal templates.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2024-55601 is a medium-severity XSS vulnerability in Hugo's internal templates where certain HTML attributes in Markdown are not properly escaped, allowing potential cross-site scripting.
Overview
CVE-2024-55601 is a cross-site scripting (XSS) vulnerability in Hugo, a popular static site generator. The issue affects Hugo versions from 0.123.0 up to (but not including) 0.139.4. The root cause is that certain HTML attributes in Markdown content are not escaped in several internal render hook templates, specifically: _default/_markup/render-link.html (since v0.123.0), _default/_markup/render-image.html (since v0.123.0), _default/_markup/render-table.html (since v0.134.0), and shortcodes/youtube.html (since v0.125.0) [1][3].
Exploitation
To exploit this vulnerability, an attacker needs the ability to supply untrusted Markdown content that is processed by Hugo using one of the affected internal templates. This could be achieved, for example, by a malicious contributor submitting content to a Hugo-based site. The attacker does not require any special network position or authentication beyond the ability to add Markdown files; exploitation occurs when the vulnerable template renders the crafted Markdown, injecting unescaped HTML attributes [4].
Impact
If successfully exploited, the attacker could inject arbitrary HTML or JavaScript into the generated static pages. This can lead to XSS attacks against site visitors, potentially enabling session hijacking, defacement, or theft of sensitive data. The severity is rated as Medium (CVSS 3.1 score 6.1) [4].
Mitigation
The vulnerability is fixed in Hugo version 0.139.4. As a workaround, users can replace the affected internal templates with custom user-defined templates or disable the internal templates entirely [3]. Site operators should update to the patched version immediately to prevent exploitation.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/gohugoio/hugoGo | >= 0.123.0, < 0.139.4 | 0.139.4 |
Affected products
4- ghsa-coords2 versions
>= 0.123.0, < 0.139.4+ 1 more
- (no CPE)range: >= 0.123.0, < 0.139.4
- (no CPE)range: < 0.0.20241213T205935-1.1
Patches
254398f8d572ctpl/tplimpl: Escape Markdown attributes in render hooks and shortcodes
7 files changed · +74 −68
hugolib/content_render_hooks_test.go+5 −5 modified@@ -90,7 +90,7 @@ baseURL="https://example.org" [markup.goldmark] [markup.goldmark.renderer] unsafe = true - + `) b.WithTemplates("index.html", ` @@ -223,16 +223,16 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== -- layouts/_default/single.html -- {{ .Title }}|{{ .Content }}|$ - + ` t.Run("Default multilingual", func(t *testing.T) { b := Test(t, files) b.AssertFileContent("public/nn/p1/index.html", - "p1|<p><a href=\"/nn/p2/\">P2</a\n></p>", "<img alt=\"Pixel\" src=\"/nn/p1/pixel.nn.png\">") + "p1|<p><a href=\"/nn/p2/\">P2</a\n></p>", "<img src=\"/nn/p1/pixel.nn.png\" alt=\"Pixel\">") b.AssertFileContent("public/en/p1/index.html", - "p1 en|<p><a href=\"/en/p2/\">P2</a\n></p>", "<img alt=\"Pixel\" src=\"/nn/p1/pixel.nn.png\">") + "p1 en|<p><a href=\"/en/p2/\">P2</a\n></p>", "<img src=\"/nn/p1/pixel.nn.png\" alt=\"Pixel\">") }) t.Run("Disabled", func(t *testing.T) { @@ -279,7 +279,7 @@ Image:  if enabled { b.AssertFileContent("public/index.html", "Link: <a href=\"/destination-%22%3C%3E\" title=\"title-"<>&\">text-"<>&</a>", - "img alt=\"alt-"<>&\" src=\"/destination-%22%3C%3E\" title=\"title-"<>&\">", + "img src=\"/destination-%22%3C%3E\" alt=\"alt-"<>&\" title=\"title-"<>&\">", "><script>", ) } else {
markup/goldmark/tables/tables_integration_test.go+8 −7 modified@@ -89,6 +89,12 @@ title = true | Codecademy Hoodie | False | 42.99 | {.foo} +## Table 2 + +a|b +---|--- +1|2 +{id="\"><script>alert()</script>"} -- layouts/_default/single.html -- Summary: {{ .Summary }} @@ -97,7 +103,8 @@ Content: {{ .Content }} ` b := hugolib.Test(t, files) - b.AssertFileContent("public/p1/index.html", "<table class=\"foo\">") + b.AssertFileContent("public/p1/index.html", `<table class="foo">`) + b.AssertFileContent("public/p1/index.html", `<table id=""><script>alert()</script>">`) } // Issue 12811. @@ -166,14 +173,8 @@ title: "Home" | Codecademy Tee | False | 19.99 | | Codecademy Hoodie | False | 42.99 | - - - - -- layouts/index.xml -- Content: {{ .Content }} - - ` b := hugolib.Test(t, files)
tpl/tplimpl/embedded/templates/_default/_markup/render-image.html+7 −6 modified@@ -1,7 +1,7 @@ {{- $u := urls.Parse .Destination -}} {{- $src := $u.String -}} {{- if not $u.IsAbs -}} - {{- $path := strings.TrimPrefix "./" $u.Path }} + {{- $path := strings.TrimPrefix "./" $u.Path -}} {{- with or (.PageInner.Resources.Get $path) (resources.Get $path) -}} {{- $src = .RelPermalink -}} {{- with $u.RawQuery -}} @@ -12,11 +12,12 @@ {{- end -}} {{- end -}} {{- end -}} -{{- $attributes := merge .Attributes (dict "alt" .Text "src" $src "title" (.Title | transform.HTMLEscape)) -}} -<img - {{- range $k, $v := $attributes -}} +<img src="{{ $src }}" alt="{{ .Text }}" + {{- with .Title }} title="{{ . }}" {{- end -}} + {{- range $k, $v := .Attributes -}} {{- if $v -}} - {{- printf " %s=%q" $k $v | safeHTMLAttr -}} + {{- printf " %s=%q" $k ($v | transform.HTMLEscape) | safeHTMLAttr -}} {{- end -}} - {{- end -}}> + {{- end -}} +> {{- /**/ -}}
tpl/tplimpl/embedded/templates/_default/_markup/render-link.html+5 −12 modified@@ -1,9 +1,9 @@ {{- $u := urls.Parse .Destination -}} {{- $href := $u.String -}} -{{- if strings.HasPrefix $u.String "#" }} - {{- $href = printf "%s#%s" .PageInner.RelPermalink $u.Fragment }} -{{- else if not $u.IsAbs -}} - {{- $path := strings.TrimPrefix "./" $u.Path }} +{{- if strings.HasPrefix $u.String "#" -}} + {{- $href = printf "%s#%s" .PageInner.RelPermalink $u.Fragment -}} +{{- else if and $href (not $u.IsAbs) -}} + {{- $path := strings.TrimPrefix "./" $u.Path -}} {{- with or ($.PageInner.GetPage $path) ($.PageInner.Resources.Get $path) @@ -18,12 +18,5 @@ {{- end -}} {{- end -}} {{- end -}} -{{- $attributes := dict "href" $href "title" (.Title | transform.HTMLEscape) -}} -<a - {{- range $k, $v := $attributes -}} - {{- if $v -}} - {{- printf " %s=%q" $k $v | safeHTMLAttr -}} - {{- end -}} - {{- end -}} - >{{ .Text }}</a> +<a href="{{ $href }}" {{- with .Title }} title="{{ . }}" {{- end }}>{{ .Text }}</a> {{- /**/ -}}
tpl/tplimpl/embedded/templates/_default/_markup/render-table.html+1 −1 modified@@ -1,7 +1,7 @@ <table {{- range $k, $v := .Attributes }} {{- if $v }} - {{- printf " %s=%q" $k $v | safeHTMLAttr }} + {{- printf " %s=%q" $k ($v | transform.HTMLEscape) | safeHTMLAttr }} {{- end }} {{- end }}> <thead>
tpl/tplimpl/embedded/templates/shortcodes/youtube.html+25 −32 modified@@ -26,7 +26,7 @@ {{- if not $pc.Disable }} {{- with $id := or (.Get "id") (.Get 0) }} - {{/* Set defaults. */}} + {{- /* Set defaults. */}} {{- $allowFullScreen := "allowfullscreen" }} {{- $autoplay := 0 }} {{- $class := "" }} @@ -70,23 +70,8 @@ {{- $start := or ($.Get "start") $start }} {{- $title := or ($.Get "title") $title }} - {{- /* Determine host. */}} - {{- $host := cond $pc.PrivacyEnhanced "www.youtube-nocookie.com" "www.youtube.com" }} - - {{- /* Set styles. */}} - {{- $divStyle := "position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;" }} - {{- $iframeStyle := "position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" }} - {{- if $class }} - {{- $iframeStyle = "" }} - {{- end }} - - {{- /* Set class or style of wrapping div element. */}} - {{- $divClassOrStyle := printf "style=%q" $divStyle }} - {{- with $class }} - {{- $divClassOrStyle = printf "class=%q" $class }} - {{- end }} - {{- /* Define src attribute. */}} + {{- $host := cond $pc.PrivacyEnhanced "www.youtube-nocookie.com" "www.youtube.com" }} {{- $src := printf "https://%s/embed/%s" $host $id }} {{- $params := dict "autoplay" $autoplay @@ -108,25 +93,33 @@ {{- $src = printf "%s?%s" $src . }} {{- end }} + {{- /* Set div attributes. */}} + {{- $divStyle := "position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;" }} + {{- if $class }} + {{- $divStyle = "" }} + {{- end }} + {{- /* Set iframe attributes. */}} - {{- $iframeAttributes := dict - "allow" "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" - "allowfullscreen" $allowFullScreen - "loading" $loading - "referrerpolicy" "strict-origin-when-cross-origin" - "src" $src - "style" $iframeStyle - "title" $title - }} + {{- $iframeStyle := "position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" }} + {{- if $class }} + {{- $iframeStyle = "" }} + {{- end }} + {{- $allow := "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" }} + {{- $referrerpolicy := "strict-origin-when-cross-origin" }} {{- /* Render. */}} - <div {{ $divClassOrStyle | safeHTMLAttr }}> + <div + {{- with $class }} class="{{ . }}" {{- end }} + {{- with $divStyle }} style="{{ . | safeCSS }}" {{- end -}} + > <iframe - {{- range $k, $v := $iframeAttributes }} - {{- if $v }} - {{- printf " %s=%q" $k $v | safeHTMLAttr }} - {{- end }} - {{- end }} + {{- with $allow }} allow="{{ . }}" {{- end }} + {{- with $allowFullScreen }} allowfullscreen="{{ . }}" {{- end }} + {{- with $loading }} loading="{{ . }}" {{- end }} + {{- with $referrerpolicy }} referrerpolicy="{{ . }}" {{- end }} + {{- with $src }} src="{{ . }}" {{- end }} + {{- with $iframeStyle}} style="{{ . | safeCSS }}" {{- end }} + {{- with $title }} title="{{ . }}" {{- end -}} ></iframe> </div> {{- else }}
tpl/tplimpl/render_hook_integration_test.go+23 −5 modified@@ -91,6 +91,9 @@ title: s1/p3 [430](p2/) [440](/s1/p2/) [450](../s1/p2/) + +// empty +[]() ` b := hugolib.Test(t, files) @@ -122,6 +125,8 @@ title: s1/p3 `<a href="/s1/p2/">430</a>`, `<a href="/s1/p2/">440</a>`, `<a href="/s1/p2/">450</a>`, + + `<a href=""></a>`, ) b.AssertFileContent("public/s1/p2/index.html", @@ -148,10 +153,17 @@ block = false [markup.goldmark.renderHooks.image] enableDefault = true -- content/p1/index.md -- +![]() +  - + + + {.foo #bar} + + +{id="\"><script>alert()</script>"} -- content/p1/pixel.png -- iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== -- layouts/_default/single.html -- @@ -160,15 +172,21 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA b := hugolib.Test(t, files) b.AssertFileContent("public/p1/index.html", - `<img alt="alt1" src="/dir/p1/pixel.png">`, - `<img alt="alt2" src="/dir/p1/pixel.png?a=b&c=d#fragment">`, + `<img src="" alt="">`, + `<img src="/dir/p1/pixel.png" alt="alt1">`, + `<img src="/dir/p1/pixel.png" alt="alt2-&<>’" title="&<>'">`, + `<img src="/dir/p1/pixel.png?a=b&c=d#fragment" alt="alt3">`, + `<img src="/dir/p1/pixel.png" alt="alt4">`, ) files = strings.Replace(files, "block = false", "block = true", -1) b = hugolib.Test(t, files) b.AssertFileContent("public/p1/index.html", - `<img alt="alt1" src="/dir/p1/pixel.png">`, - `<img alt="alt2" class="foo" id="bar" src="/dir/p1/pixel.png?a=b&c=d#fragment">`, + `<img src="" alt="">`, + `<img src="/dir/p1/pixel.png" alt="alt1">`, + `<img src="/dir/p1/pixel.png" alt="alt2-&<>’" title="&<>'">`, + `<img src="/dir/p1/pixel.png?a=b&c=d#fragment" alt="alt3" class="foo" id="bar">`, + `<img src="/dir/p1/pixel.png" alt="alt4" id=""><script>alert()</script>">`, ) }
3afe91d4b1b0Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-c2xf-9v2r-r2rxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-55601ghsaADVISORY
- github.com/gohugoio/hugo/commit/54398f8d572c689f9785d59e907fd910a23401b0nvdWEB
- github.com/gohugoio/hugo/releases/tag/v0.139.4nvdWEB
- github.com/gohugoio/hugo/security/advisories/GHSA-c2xf-9v2r-r2rxnvdWEB
- gohugo.io/getting-started/configuration-markup/nvdWEB
News mentions
0No linked articles in our index yet.