rest: add SameHost and check HTTPS downgrades against the original request GHSA-486v-q2wf-fp2r CVE-PENDING

SameHost compares two URLs by host name (case insensitively) and port
(treating the scheme's default port as no port) so redirect policies
can tell a real change of host from a server spelling its own host
differently, e.g. redirecting "https://example.com/" to
"https://EXAMPLE.com:443/".

The HTTPS downgrade check now compares the redirect target against
the original request rather than the previous hop, so a chain which
started on plaintext http, passed through an https server and came
back to http is no longer refused - nothing is being downgraded
relative to what the user asked for. A chain which started on https
and reaches http via any number of hops is still refused.
This commit is contained in:
Nick Craig-Wood
2026-09-04 19:00:22 +01:00
parent e1b0c09040
commit 31a8164815
2 changed files with 65 additions and 3 deletions
+36
View File
@@ -65,6 +65,18 @@ func TestRefuseHTTPSDowngradeRedirectFn(t *testing.T) {
next := mkRedirectReq(t, "https://example.com/b", "GET")
assert.NoError(t, RefuseHTTPSDowngradeRedirectFn(next, []*http.Request{orig}))
})
t.Run("RefusesDowngradeViaOtherHost", func(t *testing.T) {
orig := mkRedirectReq(t, "https://example.com/a", "GET")
mid := mkRedirectReq(t, "https://other.example/b", "GET")
next := mkRedirectReq(t, "http://example.com/c", "GET")
assert.ErrorIs(t, RefuseHTTPSDowngradeRedirectFn(next, []*http.Request{orig, mid}), ErrHTTPSDowngrade)
})
t.Run("AllowsPlaintextOriginViaHTTPS", func(t *testing.T) {
orig := mkRedirectReq(t, "http://example.com/a", "GET")
mid := mkRedirectReq(t, "https://other.example/b", "GET")
next := mkRedirectReq(t, "http://example.com/c", "GET")
assert.NoError(t, RefuseHTTPSDowngradeRedirectFn(next, []*http.Request{orig, mid}))
})
t.Run("TooManyRedirects", func(t *testing.T) {
next := mkRedirectReq(t, "https://example.com/b", "GET")
via := make([]*http.Request, 10)
@@ -72,6 +84,30 @@ func TestRefuseHTTPSDowngradeRedirectFn(t *testing.T) {
})
}
func TestSameHost(t *testing.T) {
for _, test := range []struct {
a, b string
want bool
}{
{"https://example.com/a", "https://example.com/b", true},
{"https://example.com/", "https://EXAMPLE.com/", true},
{"https://example.com/", "https://example.com:443/", true},
{"http://example.com/", "http://example.com:80/", true},
{"https://example.com/", "http://example.com/", false},
{"https://example.com:8443/", "https://example.com:8444/", false},
{"https://example.com/", "https://www.example.com/", false},
{"https://example.com/", "https://example.com.evil/", false},
{"http://[::1]:8080/", "http://[::1]:8080/", true},
{"http://[::1]:8080/", "http://[::1]:8081/", false},
} {
a, err := url.Parse(test.a)
require.NoError(t, err)
b, err := url.Parse(test.b)
require.NoError(t, err)
assert.Equal(t, test.want, SameHost(a, b), "%s vs %s", test.a, test.b)
}
}
// newDowngradeServers returns an HTTPS server that redirects every
// request to a plaintext HTTP server on the same host, together with a
// flag that records whether the plaintext server ever received an