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