The AddInsecureBypassPattern method of http.CrossOriginProtection, introduced in version 1.25, shows unexpected behavior.
This method is supposed to allow requests matching a given pattern to bypass protection. The issue is that more requests than expected end up being bypassed.
For example, if you define a ServeMux with two paths, /hello and /hello/:
mux := http.NewServeMux()
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("/hello"))
})
mux.HandleFunc("/hello/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("/hello/"))
})
and you configure the bypass for /hello/:
c := http.NewCrossOriginProtection()
c.AddInsecureBypassPattern("/hello/")
h := c.Handler(mux)
the result is that /hello also gets bypassed. Here's a complete example:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("/hello/"))
})
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("/hello"))
})
c := http.NewCrossOriginProtection()
c.AddInsecureBypassPattern("/hello/")
h := c.Handler(mux)
r := httptest.NewRequest("POST", "http://example.test/hello", nil)
r.Header.Set("Sec-Fetch-Site", "cross-site")
r.Header.Set("Origin", "https://evil.test")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, r)
fmt.Println(rec.Code, rec.Body.String())
}
Why this happens
CrossOriginProtection uses an internal ServeMux to check the bypass pattern. For /hello/, ServeMux would internally redirect /hello to /hello/. As a result, the internal check finds a non-empty match and CrossOriginProtection skips validation.
However, CrossOriginProtection does not actually rewrite or redirect the request path; it forwards the original one. Since the downstream mux defines a real handler for /hello, the request is served without protection.
Comment From: gabyhelp
Related Issues
Related Documentation
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
Comment From: seankhliao
cc @FiloSottile maybe this is working as intended? it is documented to work as
AddInsecureBypassPattern permits all requests that match the given pattern. The pattern syntax and precedence rules are the same as ServeMux.
Comment From: FiloSottile
Hmm, "The pattern syntax and precedence rules are the same as ServeMux." doesn't really clarify what to do about internally-generated redirects.
This is caused by the documented behavior of ServeMux.Handler:
Handler also returns the registered pattern that matches the request or, in the case of internally-generated redirects, the path that will match after following the redirect.
This has no good answer I am afraid:
- if the underlying ServeMux has both
/helloand/hello/, then bypassing both/hello/and/hellois unexpected - if the underlying ServeMux does not have
/hello, then not bypassing/hellois unexpected, because the request will be rejected instead of being redirected to the bypassed handler
However, if the ServeMux redirects are regular 302s, they would already break POSTs, so maybe it's ok to be overly protective.
The next question is whether we can change this in Go 1.25.1 or if it's already too late. I doubt there are a lot of AddInsecureBypassPattern at this point.
/cc @golang/security
Comment From: gopherbot
Change https://go.dev/cl/699275 mentions this issue: net/http: require exact match for CrossSiteProtection bypass patterns
Comment From: FiloSottile
We decided to fix this as a PUBLIC track security issue, since it's potentially surprising security-related behavior.
@gopherbot please backport to Go 1.25.
Comment From: gopherbot
Backport issue(s) opened: #75160 (for 1.25).
Remember to create the cherry-pick CL(s) as soon as the patch is submitted to master, according to https://go.dev/wiki/MinorReleases.
Comment From: gopherbot
Change https://go.dev/cl/699276 mentions this issue: [release-branch.go1.25] net/http: require exact match for CrossSiteProtection bypass patterns