]> Cypherpunks repositories - gostls13.git/commitdiff
[release-branch.go1.25] net/http: require exact match for CrossSiteProtection bypass...
authorFilippo Valsorda <filippo@golang.org>
Tue, 26 Aug 2025 20:52:39 +0000 (16:52 -0400)
committerCherry Mui <cherryyz@google.com>
Wed, 27 Aug 2025 15:45:05 +0000 (08:45 -0700)
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>

src/net/http/csrf.go
src/net/http/csrf_test.go

index 5e1b686fd1ccde085513d2f2dffe202487c3794a..d088b9b615145b5ba9de15fcd1e58f848cb599f0 100644 (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
                }
index 30986a43b9aae8adb0d565e21f0a24acd5d9be1f..a29e3ae16dff6b432decb479ac9a23ee9f74e8ca 100644 (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 {