A browser can send credentials with a request that was initiated from another site. For state-changing endpoints, accepting that request without checking its origin can expose an application to cross-site request forgery. Go’s net/http package includes CrossOriginProtection for placing that check at an HTTP handler boundary.
The type does not attempt to identify every browser request. It applies a specific policy based on request method and cross-origin signals, while allowing requests that lack those browser-origin signals. Its behavior is narrow enough that endpoint semantics still matter.
The check targets non-safe cross-origin requests
CrossOriginProtection rejects non-safe cross-origin browser requests. GET, HEAD, and OPTIONS are treated as safe methods and are allowed through the protection.
That means an application cannot use the wrapper as compensation for state changes hidden behind a GET. If a GET endpoint deletes data, changes account settings, or performs another mutation, the request remains allowed by this mechanism. HTTP method semantics are part of the security boundary.
For other methods, cross-origin detection uses browser request metadata. A Sec-Fetch-Site value can identify a cross-site request. When that signal is not available, the protection can compare the hostname represented by Origin with the request host.
Requests carrying neither Sec-Fetch-Site nor Origin are allowed. This permits clients such as command-line tools and service-to-service callers that do not send browser origin metadata, but it also defines a clear boundary: CrossOriginProtection is a browser CSRF defense, not general request authentication.
Handler wrapping keeps the policy near HTTP dispatch
The zero value is valid, so a protection value can wrap a handler without extra configuration:
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /account/email", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
var cop http.CrossOriginProtection
http.ListenAndServe(":8080", cop.Handler(mux))
}Handler checks a request before invoking the wrapped handler. A rejected request receives 403 Forbidden by default. The application handler is not called for that rejected request.
The same policy can be evaluated without wrapping a handler by calling Check. It returns an error for a request that should be rejected. This form is useful when request dispatch already has a middleware structure and the application needs explicit control over error handling.
if err := cop.Check(r); err != nil {
http.Error(w, "request rejected", http.StatusForbidden)
return
}Check only reports the decision. It does not invoke the configured denial handler.
Trusted origins expand the accepted set
Some applications intentionally accept state-changing browser requests from another origin. AddTrustedOrigin adds an exact origin value to the accepted set.
cop := http.NewCrossOriginProtection()
if err := cop.AddTrustedOrigin("https://console.example.net"); err != nil {
return err
}An origin has the form scheme://host[:port]. Trust is attached to that complete origin value rather than to a broad suffix. Treating the scheme and optional port as part of the identity avoids silently turning one trusted endpoint into a wider host pattern.
Trusted origins are a policy exception, not an authentication mechanism. A request that passes the cross-origin check still needs the application’s normal authorization and input validation.
Bypass patterns remove checks for selected routes
AddInsecureBypassPattern permits requests matching a registered pattern without cross-origin validation. Its pattern syntax and precedence follow http.ServeMux rules.
cop.AddInsecureBypassPattern("POST /hooks/provider")A bypass can fit an endpoint that has a separate request-authentication model and must accept cross-site traffic, but its scope deserves close attention. The bypass applies to every request that matches the pattern, so the route no longer receives this CSRF check.
Current Go documentation also specifies that only direct pattern matches are bypassed. A request that ServeMux would redirect after path cleaning or trailing-slash adjustment does not gain the bypass merely because the redirected path would match.
Go 1.25.0 had a security issue in AddInsecureBypassPattern that could permit a broader set of requests than intended. The issue was fixed in Go 1.25.1. Code using this API should therefore run a Go release containing that fix rather than relying on the original 1.25.0 behavior.
Denial handling can match an application’s response model
SetDenyHandler replaces the default rejection response. This can keep cross-origin failures consistent with an application’s HTTP response format while preserving the same admission decision.
cop.SetDenyHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(`{"error":"cross-origin request rejected"}`))
}))Changing the denial handler does not change which requests pass. It changes only the response produced after a request fails the check.
Configuration methods can be used while requests are being handled, and their changes apply to future requests. That property can support dynamic policy, although static startup configuration is easier to inspect when the trusted-origin and bypass sets are expected to remain fixed.
CSRF protection is one layer of request admission
Cross-origin checks answer a narrower question than authentication. A same-origin request can still be unauthorized. A non-browser request without origin headers can still be malicious. A trusted cross-origin caller can still send invalid input.
For that reason, CrossOriginProtection belongs alongside session controls, authorization, request validation, and suitable cookie settings rather than replacing them. Its specific role is to reject browser-driven state-changing requests when the available origin signals indicate that the request came from an untrusted site.
The safe-method rule is the key boundary to keep visible. When state changes remain on methods such as POST, PUT, PATCH, and DELETE, the protection can evaluate the browser-origin signals before those mutations reach application code. If mutation is exposed through a safe method, that boundary disappears regardless of how the wrapper is configured.