The template's files at a77fd30, without its history or LICENSE, after script/rename simplexcalc. Model: opus-5-5
117 lines
4.3 KiB
Go
117 lines
4.3 KiB
Go
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)
|
|
}
|