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
}
}
- bypass.Handle(pattern, noopHandler)
+ bypass.Handle(pattern, sentinelHandler)
}
// SetDenyHandler sets a handler to invoke when a request is rejected.
// 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
}
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 {
{"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 {