aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilippo Valsorda <filippo@golang.org>2025-08-26 16:52:39 -0400
committerCherry Mui <cherryyz@google.com>2025-08-27 08:45:05 -0700
commitb1959cf6f7673eaffa89bbdb00e68b30cde3aa8a (patch)
tree63750dc2d178f2438b32de70112a18cacb69d7df
parentcdd8cf4988c7c0f2bb8eb795f74c4f803c63a70d (diff)
downloadgo-b1959cf6f7673eaffa89bbdb00e68b30cde3aa8a.tar.xz
[release-branch.go1.25] net/http: require exact match for CrossSiteProtection bypass patterns
Fixes #75160 Updates #75054 Fixes CVE-2025-47910 Change-Id: I6a6a696440c45c450d2cd681f418b01aa0422a60 Reviewed-on: https://go-review.googlesource.com/c/go/+/699276 Reviewed-by: Roland Shoemaker <roland@golang.org> Reviewed-by: Cherry Mui <cherryyz@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
-rw-r--r--src/net/http/csrf.go20
-rw-r--r--src/net/http/csrf_test.go19
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 {