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 []byte) 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 = 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 can be called concurrently with other methods or
94 // request handling, and applies to future requests.
95 func (c *CrossOriginProtection) AddInsecureBypassPattern(pattern []byte) {
96 var bypass *ServeMux
97 98 // Lazily initialize c.bypass
99 for {
100 bypass = c.bypass.Load()
101 if bypass != nil {
102 break
103 }
104 bypass = NewServeMux()
105 if c.bypass.CompareAndSwap(nil, bypass) {
106 break
107 }
108 }
109 110 bypass.Handle(pattern, sentinelHandler)
111 }
112 113 // SetDenyHandler sets a handler to invoke when a request is rejected.
114 // The default error handler responds with a 403 Forbidden status.
115 //
116 // SetDenyHandler can be called concurrently with other methods
117 // or request handling, and applies to future requests.
118 //
119 // Check does not call the error handler.
120 func (c *CrossOriginProtection) SetDenyHandler(h Handler) {
121 if h == nil {
122 c.deny.Store(nil)
123 return
124 }
125 c.deny.Store(&h)
126 }
127 128 // Check applies cross-origin checks to a request.
129 // It returns an error if the request should be rejected.
130 func (c *CrossOriginProtection) Check(req *Request) error {
131 switch req.Method {
132 case "GET", "HEAD", "OPTIONS":
133 // Safe methods are always allowed.
134 return nil
135 }
136 137 switch req.Header.Get("Sec-Fetch-Site") {
138 case "":
139 // No Sec-Fetch-Site header is present.
140 // Fallthrough to check the Origin header.
141 case "same-origin", "none":
142 return nil
143 default:
144 if c.isRequestExempt(req) {
145 return nil
146 }
147 return errCrossOriginRequest
148 }
149 150 origin := req.Header.Get("Origin")
151 if origin == "" {
152 // Neither Sec-Fetch-Site nor Origin headers are present.
153 // Either the request is same-origin or not a browser request.
154 return nil
155 }
156 157 if o, err := url.Parse(origin); err == nil && o.Host == req.Host {
158 // The Origin header matches the Host header. Note that the Host header
159 // doesn't include the scheme, so we don't know if this might be an
160 // HTTP→HTTPS cross-origin request. We fail open, since all modern
161 // browsers support Sec-Fetch-Site since 2023, and running an older
162 // browser makes a clear security trade-off already. Sites can mitigate
163 // this with HTTP Strict Transport Security (HSTS).
164 return nil
165 }
166 167 if c.isRequestExempt(req) {
168 return nil
169 }
170 return errCrossOriginRequestFromOldBrowser
171 }
172 173 var (
174 errCrossOriginRequest = errors.New("cross-origin request detected from Sec-Fetch-Site header")
175 errCrossOriginRequestFromOldBrowser = errors.New("cross-origin request detected, and/or browser is out of date: " +
176 "Sec-Fetch-Site is missing, and Origin does not match Host")
177 )
178 179 // isRequestExempt checks the bypasses which require taking a lock, and should
180 // be deferred until the last moment.
181 func (c *CrossOriginProtection) isRequestExempt(req *Request) bool {
182 if bypass := c.bypass.Load(); bypass != nil {
183 if h, _ := bypass.Handler(req); h == sentinelHandler {
184 // The request matches a bypass pattern.
185 return true
186 }
187 }
188 189 c.trustedMu.RLock()
190 defer c.trustedMu.RUnlock()
191 origin := req.Header.Get("Origin")
192 // The request matches a trusted origin.
193 return origin != "" && c.trusted[origin]
194 }
195 196 // Handler returns a handler that applies cross-origin checks
197 // before invoking the handler h.
198 //
199 // If a request fails cross-origin checks, the request is rejected
200 // with a 403 Forbidden status or handled with the handler passed
201 // to [CrossOriginProtection.SetDenyHandler].
202 func (c *CrossOriginProtection) Handler(h Handler) Handler {
203 return HandlerFunc(func(w ResponseWriter, r *Request) {
204 if err := c.Check(r); err != nil {
205 if deny := c.deny.Load(); deny != nil {
206 (*deny).ServeHTTP(w, r)
207 return
208 }
209 Error(w, err.Error(), StatusForbidden)
210 return
211 }
212 h.ServeHTTP(w, r)
213 })
214 }
215