net/http: require exact match for CrossSiteProtection bypass patterns

Fixes #75054
Fixes CVE-2025-47910

Change-Id: I6a6a696440c45c450d2cd681f418b01aa0422a60
Reviewed-on: https://go-review.googlesource.com/c/go/+/699275
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
This commit is contained in:
Filippo Valsorda 2025-08-26 16:52:39 -04:00 committed by Gopher Robot
parent d19e377f6e
commit b21867b1a2
2 changed files with 31 additions and 8 deletions

View file

@ -77,13 +77,21 @@ func (c *CrossOriginProtection) AddTrustedOrigin(origin string) error {
return nil
}
var noopHandler = HandlerFunc(func(w ResponseWriter, r *Request) {})
type noopHandler struct{}
func (noopHandler) ServeHTTP(ResponseWriter, *Request) {}
var sentinelHandler Handler = &noopHandler{}
// AddInsecureBypassPattern permits all requests that match the given pattern.
// The pattern syntax and precedence rules are the same as [ServeMux].
//
// AddInsecureBypassPattern can be called concurrently with other methods
// or request handling, and applies to future requests.
// The pattern syntax and precedence rules are the same as [ServeMux]. Only
// requests that match the pattern directly are permitted. Those that ServeMux
// would redirect to a pattern (e.g. after cleaning the path or adding a
// trailing slash) are not.
//
// AddInsecureBypassPattern can be called concurrently with other methods or
// request handling, and applies to future requests.
func (c *CrossOriginProtection) AddInsecureBypassPattern(pattern string) {
var bypass *ServeMux
@ -99,7 +107,7 @@ func (c *CrossOriginProtection) AddInsecureBypassPattern(pattern string) {
}
}
bypass.Handle(pattern, noopHandler)
bypass.Handle(pattern, sentinelHandler)
}
// SetDenyHandler sets a handler to invoke when a request is rejected.
@ -172,7 +180,7 @@ var (
// be deferred until the last moment.
func (c *CrossOriginProtection) isRequestExempt(req *Request) bool {
if bypass := c.bypass.Load(); bypass != nil {
if _, pattern := bypass.Handler(req); pattern != "" {
if h, _ := bypass.Handler(req); h == sentinelHandler {
// The request matches a bypass pattern.
return true
}

View file

@ -113,6 +113,11 @@ func TestCrossOriginProtectionPatternBypass(t *testing.T) {
protection := http.NewCrossOriginProtection()
protection.AddInsecureBypassPattern("/bypass/")
protection.AddInsecureBypassPattern("/only/{foo}")
protection.AddInsecureBypassPattern("/no-trailing")
protection.AddInsecureBypassPattern("/yes-trailing/")
protection.AddInsecureBypassPattern("PUT /put-only/")
protection.AddInsecureBypassPattern("GET /get-only/")
protection.AddInsecureBypassPattern("POST /post-only/")
handler := protection.Handler(okHandler)
tests := []struct {
@ -126,13 +131,23 @@ func TestCrossOriginProtectionPatternBypass(t *testing.T) {
{"non-bypass path without sec-fetch-site", "/api/", "", http.StatusForbidden},
{"non-bypass path with cross-site", "/api/", "cross-site", http.StatusForbidden},
{"redirect to bypass path without ..", "/foo/../bypass/bar", "", http.StatusOK},
{"redirect to bypass path with trailing slash", "/bypass", "", http.StatusOK},
{"redirect to bypass path without ..", "/foo/../bypass/bar", "", http.StatusForbidden},
{"redirect to bypass path with trailing slash", "/bypass", "", http.StatusForbidden},
{"redirect to non-bypass path with ..", "/foo/../api/bar", "", http.StatusForbidden},
{"redirect to non-bypass path with trailing slash", "/api", "", http.StatusForbidden},
{"wildcard bypass", "/only/123", "", http.StatusOK},
{"non-wildcard", "/only/123/foo", "", http.StatusForbidden},
// https://go.dev/issue/75054
{"no trailing slash exact match", "/no-trailing", "", http.StatusOK},
{"no trailing slash with slash", "/no-trailing/", "", http.StatusForbidden},
{"yes trailing slash exact match", "/yes-trailing/", "", http.StatusOK},
{"yes trailing slash without slash", "/yes-trailing", "", http.StatusForbidden},
{"method-specific hit", "/post-only/", "", http.StatusOK},
{"method-specific miss (PUT)", "/put-only/", "", http.StatusForbidden},
{"method-specific miss (GET)", "/get-only/", "", http.StatusForbidden},
}
for _, tc := range tests {