Hugo: security.http.urls allow-list bypass via HTTP redirects
Description
Hugo's resources.GetRemote fails to re-validate security.http.urls on HTTP redirects (up to v0.161.1), allowing a bypass of host allow-lists to fetch from disallowed hosts.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Hugo's `resources.GetRemote` fails to re-validate `security.http.urls` on HTTP redirects (up to v0.161.1), allowing a bypass of host allow-lists to fetch from disallowed hosts.
Vulnerability
Hugo versions v0.91.0 through v0.161.1 do not re-validate the security.http.urls policy against intermediate URLs that appear during HTTP 3xx redirects. The resources.GetRemote function enforces the allow-list only on the initial URL call; subsequent redirect hops are not checked. This affects sites that use security.http.urls to constrain which external hosts Hugo can fetch remote resources from [1][2][3].
Exploitation
An attacker must control an allowed server (or its DNS or response) that the operator has included in the security.http.urls allow-list. That server returns an HTTP 3xx redirect (e.g., 302 Found) to a host that the policy was intended to forbid, such as http://localhost/ or an internal IP. Hugo follows the redirect and fetches the resource from the forbidden target without any additional check. No user interaction beyond the initial resources.GetRemote call is required [2][3][4].
Impact
A successful bypass allows Hugo to fetch and process content from hosts that the operator explicitly excluded via security.http.urls. Depending on the environment, this could lead to information disclosure (e.g., exfiltration of internal resources), SSRF, or processing of attacker-controlled content from an unintended host. The privilege level is the same as the Hugo build process (typically CI or local build user) [1][3][4].
Mitigation
Fixed in Hugo v0.162.0, released June 16, 2026 [1]. The fix installs a CheckRedirect callback on the HTTP client used by resources.GetRemote that re-evaluates security.http.urls against every redirect target and limits the redirect chain to 10 hops. No configuration change is required [1][2][3]. Users who cannot upgrade and fully trust every URL passed to resources.GetRemote are not affected, but those relying on security.http.urls as a trust boundary should update immediately [1][3][4].
AI Insight generated on Jun 16, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
186fbb0f7a8bbsecurity: Validate redirects against security.http.urls
2 files changed · +51 −0
resources/resource_factories/create/create.go+10 −0 modified@@ -16,6 +16,7 @@ package create import ( + "errors" "net/http" "os" "path" @@ -97,6 +98,15 @@ func New(rs *resources.Spec) *Client { remoteResourceLogger: rs.Logger.InfoCommand("remote"), httpClient: &http.Client{ Timeout: httpTimeout, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if err := rs.ExecHelper.Sec().CheckAllowedHTTPURL(req.URL.String()); err != nil { + return err + } + if len(via) >= 10 { + return errors.New("stopped after 10 redirects") + } + return nil + }, Transport: &httpcache.Transport{ Cache: fileCache.AsHTTPCache(), CacheKey: func(req *http.Request) string {
resources/resource_factories/create/create_integration_test.go+41 −0 modified@@ -276,6 +276,47 @@ urls = ['.*'] }) } +// Issue 14871. +func TestGetRemoteRedirectSecurityCheckIssue14871(t *testing.T) { + t.Parallel() + + // Final server: the redirect target. Its host must be denied by security.http.urls. + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/plain") + w.Write([]byte("should not reach here")) + })) + t.Cleanup(func() { target.Close() }) + + // Redirector: the allowed host. Redirects to the denied target. + redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, target.URL+"/", http.StatusFound) + })) + t.Cleanup(func() { redirector.Close() }) + + files := ` +-- hugo.toml -- +[security] +[security.http] +urls = ['REDIRECTOR'] +mediaTypes = ['text/plain'] +-- layouts/home.html -- +{{ $url := "REDIRECTOR/" }} +{{ with try (resources.GetRemote $url) }} + {{ with .Err }} + Err: {{ . }} + {{ else with .Value }} + Content: {{ .Content }} + {{ end }} +{{ end }} +` + files = strings.ReplaceAll(files, "REDIRECTOR", redirector.URL) + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/index.html", "Err:", "security.http.urls") + b.AssertFileContent("public/index.html", "! should not reach here") +} + // Issue 14611. func TestGetRemotePerRequestTimeoutBodyRead(t *testing.T) { t.Parallel()
Vulnerability mechanics
Root cause
"Missing redirect validation: `resources.GetRemote` enforced `security.http.urls` only on the initial URL, not on intermediate HTTP 3xx redirect targets."
Attack vector
An attacker who controls a server allowed by `security.http.urls` (or can manipulate its DNS or HTTP response) can make that server issue an HTTP 3xx redirect to a forbidden host — for example, `http://localhost/` or an internal IP address. Because Hugo's `resources.GetRemote` only validated the original URL against the security policy, it would follow the redirect and fetch content from the blocked target, bypassing the operator's intended host restriction [patch_id=6192778].
Affected code
The vulnerability is in `resources/resource_factories/create/create.go` in the `New` function, where the `http.Client` used by `resources.GetRemote` lacked a `CheckRedirect` callback. The initial URL was validated against `security.http.urls`, but intermediate redirect targets were not re-checked [patch_id=6192778].
What the fix does
The patch installs a `CheckRedirect` callback on the HTTP client used by `resources.GetRemote` [patch_id=6192778]. On every redirect hop, the callback re-runs `rs.ExecHelper.Sec().CheckAllowedHTTPURL(req.URL.String())` against the redirect target, enforcing `security.http.urls` on intermediate URLs. It also caps the redirect chain at 10 hops to prevent infinite loops. The accompanying integration test (`TestGetRemoteRedirectSecurityCheckIssue14871`) confirms that a redirect from an allowed host to a denied host now produces an error containing "security.http.urls" rather than fetching the forbidden content [ref_id=1].
Preconditions
- configThe site operator must have configured security.http.urls to restrict which hosts can be reached (otherwise there is no trust boundary to bypass).
- networkThe attacker must control or be able to influence the HTTP response of a server that is allowed by security.http.urls.
- inputThe site must use resources.GetRemote to fetch a URL pointing to the attacker-controlled allowed server.
Reproduction
Set up two HTTP servers: an "allowed" redirector (matching `security.http.urls`) that responds with a 302 redirect to a "denied" target (e.g., `http://localhost:1234/`). In a Hugo site, configure `[security.http] urls = ['http://allowed-server/']` and call `resources.GetRemote "http://allowed-server/"` in a template. Before the fix, Hugo fetches from the denied target; after the fix, it returns an error containing "security.http.urls" [patch_id=6192778] [ref_id=1].
Generated on Jun 16, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.