security: Validate redirects against security.http.urls

A server allowed by security.http.urls could redirect resources.GetRemote
to a host that is not. Re-run the check on each hop via CheckRedirect.

Fixes #14871
This commit is contained in:
Bjørn Erik Pedersen
2026-05-11 16:41:23 +02:00
parent 7d4af7a179
commit 86fbb0f7a8
2 changed files with 51 additions and 0 deletions
@@ -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 {
@@ -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()