diff options
Diffstat (limited to 'safeweb/http.go')
| -rw-r--r-- | safeweb/http.go | 95 |
1 files changed, 45 insertions, 50 deletions
diff --git a/safeweb/http.go b/safeweb/http.go index 143c4dcee..4d4d38874 100644 --- a/safeweb/http.go +++ b/safeweb/http.go @@ -15,11 +15,9 @@ // // # Browser Routes // -// All routes in the browser mux enforce CSRF protection using the gorilla/csrf -// package. The application must template the CSRF token into its forms using -// the [TemplateField] and [TemplateTag] APIs. Applications that are served in a -// secure context (over HTTPS) should also set the SecureContext field to true -// to ensure that the the CSRF cookies are marked as Secure. +// All routes in the browser mux are protected against CSRF attacks by +// requiring non-(GET|HEAD|OPTIONS) requests to specify the Sec-Fetch-Site header +// with the value "same-origin". // // In addition, browser routes will also have the following applied: // - Content-Security-Policy header that disallows inline scripts, framing, and third party resources. @@ -64,15 +62,11 @@ // if err := s.Serve(ln); err != nil && err != http.ErrServerClosed { // log.Fatalf("failed to serve: %v", err) // } -// -// [TemplateField]: https://pkg.go.dev/github.com/gorilla/csrf#TemplateField -// [TemplateTag]: https://pkg.go.dev/github.com/gorilla/csrf#TemplateTag package safeweb import ( "cmp" "context" - crand "crypto/rand" "fmt" "log" "maps" @@ -82,8 +76,6 @@ import ( "path" "slices" "strings" - - "github.com/gorilla/csrf" ) // CSP is the value of a Content-Security-Policy header. Keys are CSP @@ -150,10 +142,6 @@ var DefaultStrictTransportSecurityOptions = "max-age=31536000" // Config contains the configuration for a safeweb server. type Config struct { - // SecureContext specifies whether the Server is running in a secure (HTTPS) context. - // Setting this to true will cause the Server to set the Secure flag on CSRF cookies. - SecureContext bool - // BrowserMux is the HTTP handler for any routes in your application that // should only be served to browsers in a primary origin context. These // requests will be subject to CSRF protection and will have @@ -174,12 +162,6 @@ type Config struct { // No headers will be sent if no methods are provided. AccessControlAllowMethods []string - // CSRFSecret is the secret used to sign CSRF tokens. It must be 32 bytes long. - // This should be considered a sensitive value and should be kept secret. - // If this is not provided, the Server will generate a random CSRF secret on - // startup. - CSRFSecret []byte - // CSP is the Content-Security-Policy header to return with BrowserMux // responses. CSP CSP @@ -188,10 +170,6 @@ type Config struct { // inline CSS. CSPAllowInlineStyles bool - // CookiesSameSiteLax specifies whether to use SameSite=Lax in cookies. The - // default is to set SameSite=Strict. - CookiesSameSiteLax bool - // StrictTransportSecurityOptions specifies optional directives for the // Strict-Transport-Security header sent in response to requests made to the // BrowserMux when SecureContext is true. @@ -203,6 +181,16 @@ type Config struct { // Do not use the Handler field of http.Server, as it will be ignored. // Instead, set your handlers using APIMux and BrowserMux. HTTPServer *http.Server + + // ServeHSTS specifies whether to serve the Strict-Transport-Security header + // in response to requests made to the BrowserMux. + ServeHSTS bool + + // AllowSecFetchSiteSameSite specifies whether to allow requests with the + // Sec-Fetch-Site header set to "same-site " indicating that they are + // cross-origin but that their origin shares the same site (gTLD+1) with + // that of the request. + AllowSecFetchSiteSameSite bool } func (c *Config) setDefaults() error { @@ -214,13 +202,6 @@ func (c *Config) setDefaults() error { c.APIMux = &http.ServeMux{} } - if c.CSRFSecret == nil || len(c.CSRFSecret) == 0 { - c.CSRFSecret = make([]byte, 32) - if _, err := crand.Read(c.CSRFSecret); err != nil { - return fmt.Errorf("failed to generate CSRF secret: %w", err) - } - } - if c.CSP == nil { c.CSP = DefaultCSP() } @@ -231,9 +212,8 @@ func (c *Config) setDefaults() error { // Server is a safeweb server. type Server struct { Config - h *http.Server - csp string - csrfProtect func(http.Handler) http.Handler + h *http.Server + csp string } // NewServer creates a safeweb server with the provided configuration. It will @@ -252,10 +232,6 @@ func NewServer(config Config) (*Server, error) { return nil, fmt.Errorf("failed to set defaults: %w", err) } - sameSite := csrf.SameSiteStrictMode - if config.CookiesSameSiteLax { - sameSite = csrf.SameSiteLaxMode - } if config.CSPAllowInlineStyles { if _, ok := config.CSP["style-src"]; ok { config.CSP.Add("style-src", "unsafe-inline") @@ -266,9 +242,6 @@ func NewServer(config Config) (*Server, error) { s := &Server{ Config: config, csp: config.CSP.String(), - // only set Secure flag on CSRF cookies if we are in a secure context - // as otherwise the browser will reject the cookie - csrfProtect: csrf.Protect(config.CSRFSecret, csrf.Secure(config.SecureContext), csrf.SameSite(sameSite)), } s.h = cmp.Or(config.HTTPServer, &http.Server{}) if s.h.Handler != nil { @@ -318,12 +291,6 @@ func checkHandlerType(apiPattern, browserPattern string) handlerType { } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // if we are not in a secure context, signal to the CSRF middleware that - // TLS-only header checks should be skipped - if !s.Config.SecureContext { - r = csrf.PlaintextHTTPRequest(r) - } - _, bp := s.BrowserMux.Handler(r) _, ap := s.APIMux.Handler(r) switch { @@ -376,10 +343,38 @@ func (s *Server) serveBrowser(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Security-Policy", s.csp) w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Referer-Policy", "same-origin") - if s.SecureContext { + if s.ServeHSTS { w.Header().Set("Strict-Transport-Security", cmp.Or(s.StrictTransportSecurityOptions, DefaultStrictTransportSecurityOptions)) } - s.csrfProtect(s.BrowserMux).ServeHTTP(w, r) + + // Allow GET, HEAD, and OPTIONS requests to the browser mux without + // Sec-Fetch-Site header checks. + switch r.Method { + case "GET", "HEAD", "OPTIONS": + s.BrowserMux.ServeHTTP(w, r) + return + default: + } + + switch r.Header.Get("Sec-Fetch-Site") { + case "same-origin": + // allow same-origin requests + case "same-site": + // allow cross-origin, but same-site requests if configured + if !s.AllowSecFetchSiteSameSite { + http.Error(w, "forbidden cross-origin request", http.StatusForbidden) + return + } + case "cross-site", "none": + // deny cross-site requests or direct navigation non-GET requests. + http.Error(w, "forbidden cross-origin request", http.StatusForbidden) + return + default: + // deny requests with no Sec-Fetch-Site header + http.Error(w, "missing required Sec-Fetch-Site header. You might need to update your browser.", http.StatusForbidden) + return + } + s.BrowserMux.ServeHTTP(w, r) } // ServeRedirectHTTP serves a single HTTP handler on the provided listener that |
