Skip to content
CWE-352A01:2021 – Broken Access Control

Go

net/http.CrossOriginProtection in Go 1.25, gorilla/csrf for token-based protection, and why forgetting to wrap the mux is the whole risk.

The rule

Use http.NewCrossOriginProtection() from the standard library, and wrap your mux with it.

Go 1.25 added CrossOriginProtection to net/http. It is the modern, header-based approach: it checks Sec-Fetch-Site and falls back to comparing Origin against Host when that header is absent. No tokens to mint, embed, store, or rotate, and no dependency to add.

That makes Go's story unusually clean in 2026 — the recommended defence is nine lines of setup and lives in the standard library. The corresponding risk is equally simple: nothing is on by default, so an application that never wraps its mux has no protection whatsoever, and there is no framework warning to tell you.

Use gorilla/csrf in addition when you need a token — for clients that may not send Fetch Metadata, or where you want the defence that survives a proxy rewriting Origin.

The standard library

Gomain.goVulnerable
package main

import "net/http"

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("POST /account/email", changeEmail)
	mux.HandleFunc("POST /account/delete", deleteAccount)

	// No protection at all. Go adds none by default and says nothing about
	// it. changeEmail authenticates from a session cookie -- ambient -- so
	// every route here is forgeable.
	//
	// This is the most common Go CSRF finding by a wide margin, and it is a
	// finding of OMISSION: there is no wrong line to point at in review,
	// only a missing one.
	http.ListenAndServe(":8080", mux)
}

func changeEmail(w http.ResponseWriter, r *http.Request) {
	session, err := store.Get(r, "session")
	if err != nil || session.Values["userID"] == nil {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}
	updateEmail(session.Values["userID"], r.FormValue("email"))
}

Token-based protection

Gomain.goSecure
package main

import (
	"net/http"
	"os"

	"github.com/gorilla/csrf"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /account", accountForm)
	mux.HandleFunc("POST /account/email", changeEmail)

	protect := csrf.Protect(
		[]byte(os.Getenv("CSRF_AUTH_KEY")),   // 32 bytes, from the environment

		// Behind a reverse proxy, gorilla/csrf compares the request's Origin
		// or Referer against r.Host -- which may be the internal hostname
		// rather than the public one. Without TrustedOrigins this rejects
		// legitimate traffic, and the usual "fix" found online is
		// csrf.Secure(false), which is much worse.
		csrf.TrustedOrigins([]string{"app.example.com"}),

		csrf.Secure(true),
		csrf.HttpOnly(true),
		csrf.SameSite(csrf.SameSiteLaxMode),
		csrf.Path("/"),
		csrf.CookieName("__Host-csrf"),   // forbids Domain; host-only
	)

	// Layer both: headers via the stdlib, tokens via gorilla.
	cop := http.NewCrossOriginProtection()
	http.ListenAndServe(":8080", cop.Handler(protect(mux)))
}

Session cookies

Gosession.goSecure
package main

import "net/http"

// Go sets no SameSite by default -- http.Cookie.SameSite zero value is
// SameSiteDefaultMode, which emits no attribute at all. That leaves
// behaviour browser-dependent: Lax in Chromium, nothing in Firefox and
// Safari. Set it explicitly.
func setSessionCookie(w http.ResponseWriter, id string) {
	http.SetCookie(w, &http.Cookie{
		// The __Host- prefix is enforced by the browser: it requires Secure,
		// requires Path=/, and forbids Domain. That makes the cookie
		// host-only, so a sibling subdomain cannot overwrite it.
		Name:     "__Host-session",
		Value:    id,
		Path:     "/",
		// Domain deliberately unset -- __Host- would reject it anyway.
		Secure:   true,
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		MaxAge:   8 * 60 * 60,
	})
}

// Regenerate the session identifier on login. Go has no session framework in
// the standard library, so whatever you are using, check that authentication
// issues a NEW identifier rather than populating the existing one -- see the
// login-csrf guide.

Checklist

  • The mux is wrapped — cop.Handler(mux) or protect(mux) — and the wrapped handler is what is actually passed to ListenAndServe. Grep for ListenAndServe and check each one.
  • Go version is at least 1.25 if using CrossOriginProtection.
  • AddInsecureBypassPattern and TrustedOrigins lists are minimal and exact.
  • Behind a proxy: csrf.TrustedOrigins is set rather than csrf.Secure(false).
  • Session cookie sets SameSite explicitly — the zero value emits no attribute.
  • Cookie is __Host- prefixed, Secure, HttpOnly, with no Domain.
  • Routes register explicit methods. Go 1.22+ pattern syntax ("POST /path") makes this easy; older mux.Handle("/path", ...) matches every method, so the handler must check r.Method itself.
  • Handlers do not change state on GET.
  • CSRF_AUTH_KEY is 32 random bytes from the environment, not a literal in the source.
  • The session identifier is regenerated on login.