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 {