package middleware import ( "crypto/rand" "html/template" "net/http" "strings" "github.com/gorilla/csrf" ) // csrfCookieName is deliberately not the library default: a name that // says which service issued it makes a cookie jar readable, and two // services on sibling hosts do not fight over one name. const csrfCookieName = "simplexcalc_csrf" // csrfMaxAge bounds how long a token stays valid, in seconds. const csrfMaxAge = 12 * 60 * 60 // csrfKeyBytes is the key length gorilla/csrf requires. const csrfKeyBytes = 32 // CSRF protects state-changing routes (POST, PUT, PATCH, DELETE). Safe // methods pass through and are issued a token. // // The key comes from config: CSRF_KEY when set, otherwise a random key // generated here and logged as such. An ephemeral key is correct for // development and wrong for anything with more than one replica or more // than one process lifetime, because a token issued by one key is // rejected by another — the user sees a failed form submission, not a // security event. That is why it is a warning at startup and a // documented configuration key rather than a silent default. func (m *Middleware) CSRF() func(http.Handler) http.Handler { key := m.cfg.CSRFKey if m.cfg.CSRFKeyEphemeral { key = make([]byte, csrfKeyBytes) // crypto/rand.Read cannot fail on any supported platform; it // panics internally rather than returning an error a caller // might ignore. A key that is not random is not a key, so // there is nothing to fall back to here anyway. _, _ = rand.Read(key) m.log.Warn("CSRF_KEY is not set; using a random key for this process", "consequence", "tokens do not survive a restart and are not shared between replicas") } protect := csrf.Protect( key, // Secure cookies require TLS, which is absent in local // development; tying the flag to the same switch that governs // HSTS keeps "is this a production deployment" a single // decision rather than two that can disagree. csrf.Secure(m.cfg.HSTS), csrf.HttpOnly(true), csrf.SameSite(csrf.SameSiteLaxMode), csrf.Path("/"), csrf.CookieName(csrfCookieName), csrf.MaxAge(csrfMaxAge), csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { m.log.Warn("csrf rejection", "id", RequestIDFrom(r.Context()), "path", r.URL.Path, "reason", csrf.FailureReason(r).Error(), ) http.Error(w, "invalid CSRF token", http.StatusForbidden) })), ) // markScheme must be OUTSIDE protect: it sets a context value that // protect reads, so it has to run first. return func(next http.Handler) http.Handler { return markScheme(protect(next)) } } // markScheme tells gorilla/csrf whether the browser's connection was // plaintext, because the library cannot tell and assumes it was not. // // Its strict Referer check is for TLS only, and it treats every request // as TLS unless a context value says otherwise. A service behind a // TLS-terminating reverse proxy receives plaintext HTTP with an // https:// Referer — the library then applies the TLS rules to a // plaintext connection and rejects every form submission, which is a // total outage of every state-changing route rather than a subtle bug. // Left alone, the same misreading rejects plain HTTP in development for // the mirror-image reason. // // The rule: HTTPS if the connection is TLS, or if a proxy said so with // X-Forwarded-Proto. Trusting that header is safe in this one // direction — the only thing an attacker gains by setting it is // STRICTER checking of their own request. The reverse (inferring // plaintext) is what would weaken the check, and nothing a client sends // can cause it. // // A deployment behind a proxy that does not set X-Forwarded-Proto gets // the plaintext ruleset: tokens still work, and the extra Referer check // TLS would have added is not applied. Configure the proxy. func markScheme(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.TLS == nil && !strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") { r = csrf.PlaintextHTTPRequest(r) } next.ServeHTTP(w, r) }) } // CSRFField returns the hidden input for r's token, for a template to // place inside a form. Handlers call this rather than importing // gorilla/csrf, so the library stays swappable behind this package. func CSRFField(r *http.Request) template.HTML { return csrf.TemplateField(r) }