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