check / check (push) Waiting to run
Behind a trusted proxy every log line named only the proxy, so abuse could not be traced from webhooker's own logs although the rate limiters already knew the client. The access log, the rate-limit rejection lines, the CSRF warning and the receiver's request line now carry clientIP next to remoteIP. remoteIP still means the connecting peer; clientIP is the address the rate limiters key on, the forwarded client when the peer is inside TRUSTED_PROXIES, worked out once per request by the same code. The README says the field is only as trustworthy as TRUSTED_PROXIES. The access log's 2,560-byte line ceiling holds with the field charged, and a size case with an oversized X-Forwarded-For pins it. Model: opus-5-5
97 lines
3.6 KiB
Go
97 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 are logged and answered by forbidden, which must write the 403.
|
|
//
|
|
// 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(
|
|
forbidden http.Handler,
|
|
) 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
|
|
// /hook/<any length of any text>/edit lands here. The
|
|
// method and path are capped against the same budgets as
|
|
// the access log. remoteIP and clientIP are the same
|
|
// addresses the access log carries, and
|
|
// csrf.FailureReason returns one of gorilla/csrf's own
|
|
// fixed error values, so none of them is client-sized.
|
|
m.log.Warn("csrf: token validation failed",
|
|
"method", logfield.Truncate(
|
|
r.Method, maxLogMethodBytes,
|
|
),
|
|
"path", logfield.Truncate(
|
|
r.URL.Path, logfield.MaxBytes,
|
|
),
|
|
"remoteIP", RemoteIP(r),
|
|
"clientIP", ClientIP(r),
|
|
"reason", csrf.FailureReason(r),
|
|
)
|
|
forbidden.ServeHTTP(w, r)
|
|
})
|
|
|
|
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))
|
|
}
|
|
})
|
|
}
|
|
}
|