// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo package middleware import ( "fmt" "net/http" ) // CSRFConfig configures cross-origin request protection. // // This wraps net/http's CrossOriginProtection, which rejects non-safe // cross-origin browser requests by reading Sec-Fetch-Site (sent by every // browser since 2023) and falling back to comparing the Origin header's // hostname with Host. There is no token: nothing is issued, stored in a // cookie, embedded in a form, or echoed in a header. // // It replaced gorilla/csrf in 2026-09 because that library carries // GO-2025-3884 (CVE-2025-47909) with no fixed release: its trusted-origin // comparison ignored the scheme, so a policy written for https was satisfied // by plain http. AddTrustedOrigin here requires a full origin, scheme // included, which is the defect's direct answer. // // Two consequences worth knowing. GET, HEAD and OPTIONS are always allowed, so // no handler may change state on those methods. And a request carrying neither // Sec-Fetch-Site nor Origin is treated as same-origin or non-browser and // allowed, which is what lets server-to-server callers through. type CSRFConfig struct { // TrustedOrigins are additional origins allowed to make non-safe // requests, each a full origin such as "https://console.example.coop". // An entry without a scheme is rejected at construction. TrustedOrigins []string // BypassPatterns are net/http.ServeMux patterns exempted from the check // entirely, for endpoints that authenticate their caller some other way // (a provider webhook verifying its own signature). Each one is a // deliberate hole; see the server's exempt-path list for the contract. BypassPatterns []string // DenyHandler serves a rejected request. When nil the standard library's // default 403 is used. DenyHandler http.Handler } // CSRF returns middleware enforcing cross-origin protection. // // An invalid trusted origin is a configuration error the caller must handle; // it is never silently dropped, because a trusted origin that fails to // register would leave a legitimate deployment rejecting its own form posts. func CSRF(config CSRFConfig) (Middleware, error) { protection := http.NewCrossOriginProtection() for _, origin := range config.TrustedOrigins { if err := protection.AddTrustedOrigin(origin); err != nil { return nil, fmt.Errorf("trusted origin %q: %w", origin, err) } } for _, pattern := range config.BypassPatterns { protection.AddInsecureBypassPattern(pattern) } if config.DenyHandler != nil { protection.SetDenyHandler(config.DenyHandler) } return func(next http.Handler) http.Handler { return protection.Handler(next) }, nil }