Cross-Origin Protection
Cross-Site Request Forgery (CSRF) protection using Go 1.25's built-in
http.CrossOriginProtection.
Detects cross-origin browser requests via the Sec-Fetch-Site header
(supported in all browsers since 2023) and rejects them with 403 Forbidden.
No cookies, no tokens, no storage.
Usage
r.Use(middleware.CrossOriginProtection())
How It Works
The middleware uses http.CrossOriginProtection.Check
which applies the following logic:
- Safe methods (GET, HEAD, OPTIONS) are always allowed
Sec-Fetch-Site: same-originornone→ allowedSec-Fetch-Site: cross-siteorsame-site→ rejected (403)- No
Sec-Fetch-Site+OriginmatchesHost→ allowed - No
Sec-Fetch-Site+ noOrigin→ allowed (assumed same-origin or non-browser) - No
Sec-Fetch-Site+Originmismatch → rejected (403)
Configuration
config := middleware.DefaultCrossOriginProtectionConfig()
config.TrustedOrigins = []string{"https://api.example.com"}
r.Use(middleware.CrossOriginProtectionWithConfig(config))
Options
| Option | Default | Description |
|---|---|---|
TrustedOrigins | nil | Origins that bypass CSRF checks (e.g. OAuth callbacks) |
InsecureBypassPatterns | nil | Route patterns that bypass CSRF (e.g. webhooks) |
DenyHandler | nil | Custom handler for rejected requests (default: 403) |
Skipper | nil | Skip CSRF for matching requests |
Trusted Origins
Allow specific origins to bypass CSRF checks - useful for API clients, OAuth callbacks, or microservice-to-microservice requests:
config.TrustedOrigins = []string{
"https://app.example.com",
"https://api.partner.com",
}
r.Use(middleware.CrossOriginProtectionWithConfig(config))
Insecure Bypass Patterns (Not Recommended)
These patterns skip CSRF checks entirely - use only for routes like webhooks or health checks where the caller cannot send standard browser headers:
config.InsecureBypassPatterns = []string{"POST /webhook", "GET /health"}
r.Use(middleware.CrossOriginProtectionWithConfig(config))
Pattern syntax follows http.ServeMux rules. Only requests matching the exact
pattern are bypassed. The "Insecure" prefix warns that this disables protection
for those routes - prefer Skipper or TrustedOrigins when possible.
Skipping Routes
config.Skipper = zen.SkipPaths("/health", "/metrics")
r.Use(middleware.CrossOriginProtectionWithConfig(config))
Custom Deny Handler
config.DenyHandler = func(c *zen.Ctx) {
c.JSON(403, map[string]string{"error": "cross-origin request rejected"})
}
r.Use(middleware.CrossOriginProtectionWithConfig(config))
Defense in Depth
http.CrossOriginProtection relies on the Sec-Fetch-Site header, supported
by ~92% of browsers, and the Origin header, supported by ~95%. It blocks
attacks from modern browsers, but legacy browsers (pre-2020) may not send
either header.
For full coverage, combine Cross-Origin Protection with:
SameSite Cookies
Set SameSite=Lax or SameSite=Strict on session/auth cookies to prevent
cross-site request forgery from legacy browsers (including Firefox v60-69):
r.POST("/login", func(c *zen.Ctx) {
// ... authenticate user, get session token ...
http.SetCookie(c.Response, &http.Cookie{
Name: "session",
Value: token,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
c.String(200, "ok")
})
All major browsers that support TLS 1.3 also support SameSite cookies.
TLS 1.3
Enforce TLS 1.3 as the minimum version on your server. Older browsers that
don't support TLS 1.3 cannot connect, and all modern browsers that support
TLS 1.3 also support the Sec-Fetch-Site or Origin headers:
r.RunTLS("cert.pem", "key.pem")
HSTS
Prevent downgrade attacks by including the Strict-Transport-Security header
on all HTTPS responses. This mitigates the risk of cross-origin requests from
an HTTP version of your origin when no Sec-Fetch-Site header is present:
r.Use(func(c *zen.Ctx) {
c.Response.Header().Set("Strict-Transport-Security",
"max-age=63072000; includeSubDomains")
c.Next()
})
Requirements
- Go 1.25 or later (uses
net/http.CrossOriginProtection)