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
/hello
and/hello/
, then bypassing both/hello/
and/hello
is unexpected - if the underlying ServeMux does not have
/hello
, then not bypassing/hello
is 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