Skip to main content

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-origin or none → allowed
  • Sec-Fetch-Site: cross-site or same-site → rejected (403)
  • No Sec-Fetch-Site + Origin matches Host → allowed
  • No Sec-Fetch-Site + no Origin → allowed (assumed same-origin or non-browser)
  • No Sec-Fetch-Site + Origin mismatch → rejected (403)

Configuration

config := middleware.DefaultCrossOriginProtectionConfig()
config.TrustedOrigins = []string{"https://api.example.com"}
r.Use(middleware.CrossOriginProtectionWithConfig(config))

Options

OptionDefaultDescription
TrustedOriginsnilOrigins that bypass CSRF checks (e.g. OAuth callbacks)
InsecureBypassPatternsnilRoute patterns that bypass CSRF (e.g. webhooks)
DenyHandlernilCustom handler for rejected requests (default: 403)
SkippernilSkip 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))

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)