VYPR
Medium severityOSV Advisory· Published Dec 9, 2024· Updated Apr 15, 2026

CVE-2024-55601

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.

PackageAffected versionsPatched versions
github.com/gohugoio/hugoGo
>= 0.123.0, < 0.139.40.139.4

Affected products

4

Patches

2
54398f8d572c

tpl/tplimpl: Escape Markdown attributes in render hooks and shortcodes

https://github.com/gohugoio/hugoJoe MooringDec 4, 2024via ghsa
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: ![alt-"<>&](/destination-"<> 'title-"<>&')
     			if enabled {
     				b.AssertFileContent("public/index.html",
     					"Link: <a href=\"/destination-%22%3C%3E\" title=\"title-&#34;&lt;&gt;&amp;\">text-&quot;&lt;&gt;&amp;</a>",
    -					"img alt=\"alt-&quot;&lt;&gt;&amp;\" src=\"/destination-%22%3C%3E\" title=\"title-&#34;&lt;&gt;&amp;\">",
    +					"img src=\"/destination-%22%3C%3E\" alt=\"alt-&quot;&lt;&gt;&amp;\" title=\"title-&#34;&lt;&gt;&amp;\">",
     					"&gt;&lt;script&gt;",
     				)
     			} 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="&#34;&gt;&lt;script&gt;alert()&lt;/script&gt;">`)
     }
     
     // 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 --
    +![]()
    +
     ![alt1](./pixel.png)
     
    -![alt2](pixel.png?a=b&c=d#fragment)
    +![alt2-&<>'](pixel.png "&<>'")
    +
    +![alt3](pixel.png?a=b&c=d#fragment)
     {.foo #bar}
    +
    +![alt4](pixel.png)
    +{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-&amp;&lt;&gt;&rsquo;" title="&amp;&lt;&gt;&#39;">`,
    +		`<img src="/dir/p1/pixel.png?a=b&amp;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-&amp;&lt;&gt;&rsquo;" title="&amp;&lt;&gt;&#39;">`,
    +		`<img src="/dir/p1/pixel.png?a=b&amp;c=d#fragment" alt="alt3" class="foo" id="bar">`,
    +		`<img src="/dir/p1/pixel.png" alt="alt4" id="&#34;&gt;&lt;script&gt;alert()&lt;/script&gt;">`,
     	)
     }
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.