diff options
| author | Filippo Valsorda <filippo@golang.org> | 2025-08-26 16:52:39 -0400 |
|---|---|---|
| committer | Gopher Robot <gobot@golang.org> | 2025-08-27 08:03:34 -0700 |
| commit | b21867b1a2a8e276257e3cb81f4a1dc7e8f9e2cd (patch) | |
| tree | 370e71cf26dab0ef6e1b636116f4db6cc9d1520f /src | |
| parent | d19e377f6ea3b84e94d309894419f2995e7b56bd (diff) | |
| download | go-b21867b1a2a8e276257e3cb81f4a1dc7e8f9e2cd.tar.xz | |
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>
Diffstat (limited to 'src')
| -rw-r--r-- | src/net/http/csrf.go | 20 | ||||
| -rw-r--r-- | src/net/http/csrf_test.go | 19 |
2 files changed, 31 insertions, 8 deletions
diff --git a/src/net/http/csrf.go b/src/net/http/csrf.go index 5e1b686fd1..d088b9b615 100644 --- a/src/net/http/csrf.go +++ b/src/net/http/csrf.go @@ -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 } diff --git a/src/net/http/csrf_test.go b/src/net/http/csrf_test.go index 30986a43b9..a29e3ae16d 100644 --- a/src/net/http/csrf_test.go +++ b/src/net/http/csrf_test.go @@ -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 { |
