All checks were successful
check / check (push) Successful in 3m28s
Two places decided whether a request was TLS, by two different means, and they disagreed. The session cookie's Secure attribute was fixed at startup from !Config.IsDev(). "dev" is the environment when WEBHOOKER_ENVIRONMENT is unset, so a deployment terminating TLS at a proxy without also setting the environment shipped the authentication cookie with no Secure attribute -- on the same response as a CSRF cookie that had one. It failed silently: everything kept working, so nothing prompted anyone to look. The CSRF middleware's per-request check compared X-Forwarded-Proto with == "https" exactly, so "HTTPS", "https, http" and "https,https" all took the plaintext path. Uppercase is legal for a case-insensitive token and the comma forms are what a proxy chained behind another proxy emits by appending rather than replacing. On that path gorilla/csrf stops enforcing the strict Referer check on a site that genuinely is HTTPS. Both now go through internal/reqtls.IsTLS, which folds case and takes the leftmost comma-separated element -- the hop nearest the client, and so the one a cookie's Secure attribute is about. A third package is needed because internal/middleware already imports internal/session, so session cannot import middleware back. Per-request beat a startup warning for the session cookie because it turned out to need no restructuring: gorilla/sessions gives every session its own copy of the store's Options and renders the cookie from that copy, and every session-cookie write here already goes through Session.Save or Session.Regenerate, both of which hold the request. The store's template Secure becomes true so that a write path added later which forgets to track the transport fails visibly instead of silently dropping Secure. The flag tracks the transport in both directions rather than latching on. Secure over plaintext is discarded by the browser without an error, which would make a plain-HTTP local run impossible to log into -- and would also void the deletion cookies in Destroy and Regenerate, leaving a session the user just tried to end still live. A third site that makes this decision, internal/handlers' BaseURL construction, assigns the raw header straight into the URL scheme. It is left alone here and filed separately.
94 lines
3.6 KiB
Go
94 lines
3.6 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gorilla/csrf"
|
|
"sneak.berlin/go/webhooker/internal/logfield"
|
|
"sneak.berlin/go/webhooker/internal/reqtls"
|
|
)
|
|
|
|
// CSRFToken retrieves the CSRF token from the request context.
|
|
// Returns an empty string if the gorilla/csrf middleware has not run.
|
|
func CSRFToken(r *http.Request) string {
|
|
return csrf.Token(r)
|
|
}
|
|
|
|
// CSRF returns middleware that provides CSRF protection using the
|
|
// gorilla/csrf library. The middleware uses the session authentication
|
|
// key to sign a CSRF cookie and validates a masked token submitted via
|
|
// the "csrf_token" form field (or the "X-CSRF-Token" header) on
|
|
// POST/PUT/PATCH/DELETE requests. Requests with an invalid or missing
|
|
// token receive a 403 Forbidden response.
|
|
//
|
|
// The middleware detects the client-facing transport protocol
|
|
// per-request via reqtls.IsTLS, the single TLS predicate the session
|
|
// cookie also uses. This allows correct behavior in all deployment
|
|
// scenarios:
|
|
//
|
|
// - Direct HTTPS: strict Referer/Origin checks, Secure cookies.
|
|
// - Behind a TLS-terminating reverse proxy: strict checks (the
|
|
// browser is on HTTPS, so Origin/Referer headers use https://),
|
|
// Secure cookies (the browser sees HTTPS from the proxy).
|
|
// - Direct HTTP: relaxed Referer/Origin checks via PlaintextHTTPRequest,
|
|
// non-Secure cookies so the browser sends them over HTTP.
|
|
//
|
|
// Two gorilla/csrf instances are maintained — one with Secure cookies
|
|
// (for TLS) and one without (for plaintext HTTP) — because the
|
|
// csrf.Secure option is set at creation time, not per-request.
|
|
func (m *Middleware) CSRF() func(http.Handler) http.Handler {
|
|
csrfErrorHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// CSRF is registered ahead of RequireAuth on every route
|
|
// group that uses it, so this WARN is reachable by an
|
|
// unauthenticated client: a POST with no token to
|
|
// /source/<any length of any text>/edit lands here. The
|
|
// method and path are capped against the same budgets as
|
|
// the access log. remote_addr is set by net/http from the
|
|
// accepted connection rather than by the client, and
|
|
// csrf.FailureReason returns one of gorilla/csrf's own
|
|
// fixed error values, so neither is client-sized.
|
|
m.log.Warn("csrf: token validation failed",
|
|
"method", logfield.Truncate(
|
|
r.Method, maxLogMethodBytes,
|
|
),
|
|
"path", logfield.Truncate(
|
|
r.URL.Path, logfield.MaxBytes,
|
|
),
|
|
"remote_addr", r.RemoteAddr,
|
|
"reason", csrf.FailureReason(r),
|
|
)
|
|
http.Error(w, "Forbidden - invalid CSRF token", http.StatusForbidden)
|
|
})
|
|
|
|
key := m.session.GetKey()
|
|
baseOpts := []csrf.Option{
|
|
csrf.FieldName("csrf_token"),
|
|
csrf.SameSite(csrf.SameSiteLaxMode),
|
|
csrf.Path("/"),
|
|
csrf.ErrorHandler(csrfErrorHandler),
|
|
}
|
|
|
|
// Two middleware instances with different Secure flags but the
|
|
// same signing key, so cookies are interchangeable between them.
|
|
tlsProtect := csrf.Protect(key, append(baseOpts, csrf.Secure(true))...)
|
|
httpProtect := csrf.Protect(key, append(baseOpts, csrf.Secure(false))...)
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
tlsCSRF := tlsProtect(next)
|
|
httpCSRF := httpProtect(next)
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if reqtls.IsTLS(r) {
|
|
// Client is on TLS (directly or via reverse proxy).
|
|
// Use Secure cookies and strict Origin/Referer checks.
|
|
tlsCSRF.ServeHTTP(w, r)
|
|
} else {
|
|
// Plaintext HTTP: use non-Secure cookies and tell
|
|
// gorilla/csrf to use "http" for scheme comparisons,
|
|
// skipping the strict Referer check that assumes TLS.
|
|
httpCSRF.ServeHTTP(w, csrf.PlaintextHTTPRequest(r))
|
|
}
|
|
})
|
|
}
|
|
}
|