Source file src/net/http/csrf.go

     1  // Copyright 2025 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package http
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"net/url"
    11  	"sync"
    12  	"sync/atomic"
    13  )
    14  
    15  // CrossOriginProtection implements protections against [Cross-Site Request
    16  // Forgery (CSRF)] by rejecting non-safe cross-origin browser requests.
    17  //
    18  // Cross-origin requests are currently detected with the [Sec-Fetch-Site]
    19  // header, available in all browsers since 2023, or by comparing the hostname of
    20  // the [Origin] header with the Host header.
    21  //
    22  // The GET, HEAD, and OPTIONS methods are [safe methods] and are always allowed.
    23  // It's important that applications do not perform any state changing actions
    24  // due to requests with safe methods.
    25  //
    26  // Requests without Sec-Fetch-Site or Origin headers are currently assumed to be
    27  // either same-origin or non-browser requests, and are allowed.
    28  //
    29  // The zero value of CrossOriginProtection is valid and has no trusted origins
    30  // or bypass patterns.
    31  //
    32  // [Sec-Fetch-Site]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Site
    33  // [Origin]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin
    34  // [Cross-Site Request Forgery (CSRF)]: https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF
    35  // [safe methods]: https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP
    36  type CrossOriginProtection struct {
    37  	bypass    atomic.Pointer[ServeMux]
    38  	trustedMu sync.RWMutex
    39  	trusted   map[string]bool
    40  	deny      atomic.Pointer[Handler]
    41  }
    42  
    43  // NewCrossOriginProtection returns a new [CrossOriginProtection] value.
    44  func NewCrossOriginProtection() *CrossOriginProtection {
    45  	return &CrossOriginProtection{}
    46  }
    47  
    48  // AddTrustedOrigin allows all requests with an [Origin] header
    49  // which exactly matches the given value.
    50  //
    51  // Origin header values are of the form "scheme://host[:port]".
    52  //
    53  // AddTrustedOrigin can be called concurrently with other methods
    54  // or request handling, and applies to future requests.
    55  //
    56  // [Origin]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin
    57  func (c *CrossOriginProtection) AddTrustedOrigin(origin string) error {
    58  	u, err := url.Parse(origin)
    59  	if err != nil {
    60  		return fmt.Errorf("invalid origin %q: %w", origin, err)
    61  	}
    62  	if u.Scheme == "" {
    63  		return fmt.Errorf("invalid origin %q: scheme is required", origin)
    64  	}
    65  	if u.Host == "" {
    66  		return fmt.Errorf("invalid origin %q: host is required", origin)
    67  	}
    68  	if u.Path != "" || u.RawQuery != "" || u.Fragment != "" {
    69  		return fmt.Errorf("invalid origin %q: path, query, and fragment are not allowed", origin)
    70  	}
    71  	c.trustedMu.Lock()
    72  	defer c.trustedMu.Unlock()
    73  	if c.trusted == nil {
    74  		c.trusted = make(map[string]bool)
    75  	}
    76  	c.trusted[origin] = true
    77  	return nil
    78  }
    79  
    80  type noopHandler struct{}
    81  
    82  func (noopHandler) ServeHTTP(ResponseWriter, *Request) {}
    83  
    84  var sentinelHandler Handler = &noopHandler{}
    85  
    86  // AddInsecureBypassPattern permits all requests that match the given pattern.
    87  //
    88  // The pattern syntax and precedence rules are the same as [ServeMux]. Only
    89  // requests that match the pattern directly are permitted. Those that ServeMux
    90  // would redirect to a pattern (e.g. after cleaning the path or adding a
    91  // trailing slash) are not.
    92  //
    93  // AddInsecureBypassPattern panics if the pattern conflicts with one already
    94  // registered, or if the pattern is syntactically invalid (for example, an
    95  // improperly formed wildcard).
    96  //
    97  // AddInsecureBypassPattern can be called concurrently with other methods or
    98  // request handling, and applies to future requests.
    99  func (c *CrossOriginProtection) AddInsecureBypassPattern(pattern string) {
   100  	var bypass *ServeMux
   101  
   102  	// Lazily initialize c.bypass
   103  	for {
   104  		bypass = c.bypass.Load()
   105  		if bypass != nil {
   106  			break
   107  		}
   108  		bypass = NewServeMux()
   109  		if c.bypass.CompareAndSwap(nil, bypass) {
   110  			break
   111  		}
   112  	}
   113  
   114  	bypass.Handle(pattern, sentinelHandler)
   115  }
   116  
   117  // SetDenyHandler sets a handler to invoke when a request is rejected.
   118  // The default error handler responds with a 403 Forbidden status.
   119  //
   120  // SetDenyHandler can be called concurrently with other methods
   121  // or request handling, and applies to future requests.
   122  //
   123  // Check does not call the error handler.
   124  func (c *CrossOriginProtection) SetDenyHandler(h Handler) {
   125  	if h == nil {
   126  		c.deny.Store(nil)
   127  		return
   128  	}
   129  	c.deny.Store(&h)
   130  }
   131  
   132  // Check applies cross-origin checks to a request.
   133  // It returns an error if the request should be rejected.
   134  func (c *CrossOriginProtection) Check(req *Request) error {
   135  	switch req.Method {
   136  	case "GET", "HEAD", "OPTIONS":
   137  		// Safe methods are always allowed.
   138  		return nil
   139  	}
   140  
   141  	switch req.Header.Get("Sec-Fetch-Site") {
   142  	case "":
   143  		// No Sec-Fetch-Site header is present.
   144  		// Fallthrough to check the Origin header.
   145  	case "same-origin", "none":
   146  		return nil
   147  	default:
   148  		if c.isRequestExempt(req) {
   149  			return nil
   150  		}
   151  		return errCrossOriginRequest
   152  	}
   153  
   154  	origin := req.Header.Get("Origin")
   155  	if origin == "" {
   156  		// Neither Sec-Fetch-Site nor Origin headers are present.
   157  		// Either the request is same-origin or not a browser request.
   158  		return nil
   159  	}
   160  
   161  	if o, err := url.Parse(origin); err == nil && o.Host == req.Host {
   162  		// The Origin header matches the Host header. Note that the Host header
   163  		// doesn't include the scheme, so we don't know if this might be an
   164  		// HTTP→HTTPS cross-origin request. We fail open, since all modern
   165  		// browsers support Sec-Fetch-Site since 2023, and running an older
   166  		// browser makes a clear security trade-off already. Sites can mitigate
   167  		// this with HTTP Strict Transport Security (HSTS).
   168  		return nil
   169  	}
   170  
   171  	if c.isRequestExempt(req) {
   172  		return nil
   173  	}
   174  	return errCrossOriginRequestFromOldBrowser
   175  }
   176  
   177  var (
   178  	errCrossOriginRequest               = errors.New("cross-origin request detected from Sec-Fetch-Site header")
   179  	errCrossOriginRequestFromOldBrowser = errors.New("cross-origin request detected, and/or browser is out of date: " +
   180  		"Sec-Fetch-Site is missing, and Origin does not match Host")
   181  )
   182  
   183  // isRequestExempt checks the bypasses which require taking a lock, and should
   184  // be deferred until the last moment.
   185  func (c *CrossOriginProtection) isRequestExempt(req *Request) bool {
   186  	if bypass := c.bypass.Load(); bypass != nil {
   187  		if h, _ := bypass.Handler(req); h == sentinelHandler {
   188  			// The request matches a bypass pattern.
   189  			return true
   190  		}
   191  	}
   192  
   193  	c.trustedMu.RLock()
   194  	defer c.trustedMu.RUnlock()
   195  	origin := req.Header.Get("Origin")
   196  	// The request matches a trusted origin.
   197  	return origin != "" && c.trusted[origin]
   198  }
   199  
   200  // Handler returns a handler that applies cross-origin checks
   201  // before invoking the handler h.
   202  //
   203  // If a request fails cross-origin checks, the request is rejected
   204  // with a 403 Forbidden status or handled with the handler passed
   205  // to [CrossOriginProtection.SetDenyHandler].
   206  func (c *CrossOriginProtection) Handler(h Handler) Handler {
   207  	return HandlerFunc(func(w ResponseWriter, r *Request) {
   208  		if err := c.Check(r); err != nil {
   209  			if deny := c.deny.Load(); deny != nil {
   210  				(*deny).ServeHTTP(w, r)
   211  				return
   212  			}
   213  			Error(w, err.Error(), StatusForbidden)
   214  			return
   215  		}
   216  		h.ServeHTTP(w, r)
   217  	})
   218  }
   219  

View as plain text