github.com/google/go-safeweb@v0.0.0-20231219055052-64d8cfc90fbb/safehttp/plugins/cors/cors.go (about) 1 // Copyright 2020 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package cors provides a safehttp.Interceptor that handles CORS requests. 16 package cors 17 18 import ( 19 "log" 20 "net/textproto" 21 "strconv" 22 "strings" 23 24 "github.com/google/go-safeweb/safehttp" 25 ) 26 27 const requiredHeader string = "X-Cors" 28 29 var disallowedContentTypes = map[string]bool{ 30 "application/x-www-form-urlencoded": true, 31 "multipart/form-data": true, 32 "text/plain": true, 33 } 34 35 // Interceptor handles CORS requests based on its settings. 36 // 37 // For more info about CORS, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS 38 // 39 // # Constraints 40 // 41 // The content types "application/x-www-form-urlencoded", "multipart/form-data" 42 // and "text/plain" are banned and will result in a 415 Unsupported Media Type 43 // response. 44 // 45 // Each CORS request must contain the header "X-Cors: 1". 46 // 47 // The HEAD request method is disallowed. 48 // 49 // All of this is to prevent XSRF. 50 type Interceptor struct { 51 // AllowedOrigins determines which origins should be allowed in the 52 // Access-Control-Allow-Origin header. 53 AllowedOrigins map[string]bool 54 // ExposedHeaders determines which headers should be set in the 55 // Access-Control-Expose-Headers header. This controls which headers are 56 // accessible by JavaScript in the response. 57 // 58 // If ExposedHeaders is nil, then the header is not set, meaning that nothing 59 // is exposed. 60 ExposedHeaders []string 61 // AllowCredentials determines if Access-Control-Allow-Credentials should be 62 // set to true, which would allow cookies to be attached to requests. 63 AllowCredentials bool 64 // MaxAge sets the Access-Control-Max-Age header, indicating how many seconds 65 // the results of a preflight request can be cached. 66 // 67 // MaxAge=0 results in MaxAge: 5, the default used by Chromium according to 68 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age 69 MaxAge int 70 allowedHeaders map[string]bool 71 } 72 73 var _ safehttp.Interceptor = &Interceptor{} 74 75 // Default creates a CORS Interceptor with default settings. 76 // Those defaults are: 77 // - No Exposed Headers 78 // - No Allowed Headers 79 // - AllowCredentials: false 80 // - MaxAge: 5 seconds 81 func Default(allowedOrigins ...string) *Interceptor { 82 ao := map[string]bool{} 83 for _, o := range allowedOrigins { 84 ao[o] = true 85 } 86 return &Interceptor{ 87 AllowedOrigins: ao, 88 allowedHeaders: map[string]bool{}, 89 } 90 } 91 92 // SetAllowedHeaders sets the headers allowed in the Access-Control-Allow-Headers 93 // header. The headers are first canonicalized using textproto.CanonicalMIMEHeaderKey. 94 // The wildcard "*" is not allowed. 95 func (it *Interceptor) SetAllowedHeaders(headers ...string) { 96 it.allowedHeaders = map[string]bool{} 97 for _, h := range headers { 98 if h == "*" { 99 continue 100 } 101 it.allowedHeaders[textproto.CanonicalMIMEHeaderKey(h)] = true 102 } 103 } 104 105 // Before handles the IncomingRequest according to the settings specified in the 106 // Interceptor and sets the appropriate subset of the following headers: 107 // 108 // - Access-Control-Allow-Credentials 109 // - Access-Control-Allow-Headers 110 // - Access-Control-Allow-Methods 111 // - Access-Control-Allow-Origin 112 // - Access-Control-Expose-Headers 113 // - Access-Control-Max-Age 114 // - Vary 115 func (it *Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, _ safehttp.InterceptorConfig) safehttp.Result { 116 origin := r.Header.Get("Origin") 117 if origin != "" && !it.AllowedOrigins[origin] { 118 if safehttp.IsLocalDev() { 119 log.Println("cors plugin blocked a request due to a mismatching Origin header.") 120 } 121 return w.WriteError(safehttp.StatusForbidden) 122 } 123 h := w.Header() 124 allowOrigin := h.Claim("Access-Control-Allow-Origin") 125 if h.IsClaimed("Vary") { 126 if safehttp.IsLocalDev() { 127 log.Println("cors plugin failed to claim the Vary header") 128 } 129 return w.WriteError(safehttp.StatusInternalServerError) 130 } 131 132 allowCredentials := h.Claim("Access-Control-Allow-Credentials") 133 134 var status safehttp.StatusCode 135 switch r.Method() { 136 case safehttp.MethodOptions: 137 status = it.preflight(w, r) 138 case safehttp.MethodHead: 139 status = safehttp.StatusMethodNotAllowed 140 default: 141 status = it.request(w, r) 142 } 143 144 if status != 0 && status != safehttp.StatusNoContent { 145 if safehttp.IsLocalDev() { 146 log.Println("cors plugin blocked a potentially malicious request") 147 } 148 return w.WriteError(status) 149 } 150 151 if origin != "" { 152 allowOrigin([]string{origin}) 153 appendToVary(w, "Origin") 154 } 155 if r.Header.Get("Cookie") != "" && it.AllowCredentials { 156 // TODO: handle other credentials than cookies: 157 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials 158 allowCredentials([]string{"true"}) 159 } 160 161 if status == safehttp.StatusNoContent { 162 return w.Write(safehttp.NoContentResponse{}) 163 } 164 return safehttp.NotWritten() 165 } 166 167 // Commit is a no-op, required to satisfy the safehttp.Interceptor interface. 168 func (it *Interceptor) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg safehttp.InterceptorConfig) { 169 } 170 171 // Match returns false since there are no supported configurations. 172 func (*Interceptor) Match(safehttp.InterceptorConfig) bool { 173 return false 174 } 175 176 func appendToVary(w safehttp.ResponseWriter, val string) { 177 h := w.Header() 178 if curr := h.Get("Vary"); curr != "" { 179 h.Set("Vary", curr+", "+val) 180 } else { 181 h.Set("Vary", val) 182 } 183 } 184 185 // preflight handles requests that have the method OPTIONS. 186 func (it *Interceptor) preflight(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.StatusCode { 187 rh := r.Header 188 if rh.Get("Origin") == "" { 189 return safehttp.StatusForbidden 190 } 191 192 method := rh.Get("Access-Control-Request-Method") 193 if method == "" || method == safehttp.MethodHead { 194 return safehttp.StatusForbidden 195 } 196 197 headers := rh.Get("Access-Control-Request-Headers") 198 if headers != "" { 199 for _, h := range strings.Split(headers, ", ") { 200 h = textproto.CanonicalMIMEHeaderKey(h) 201 if !it.allowedHeaders[h] && h != requiredHeader { 202 return safehttp.StatusForbidden 203 } 204 } 205 } 206 207 wh := w.Header() 208 allowMethods := wh.Claim("Access-Control-Allow-Methods") 209 allowHeaders := wh.Claim("Access-Control-Allow-Headers") 210 maxAge := wh.Claim("Access-Control-Max-Age") 211 212 allowMethods([]string{method}) 213 if headers != "" { 214 allowHeaders([]string{headers}) 215 } 216 n := it.MaxAge 217 if n == 0 { 218 n = 5 219 } 220 maxAge([]string{strconv.Itoa(n)}) 221 return safehttp.StatusNoContent 222 } 223 224 // request handles all requests that are not preflight requests. 225 func (it *Interceptor) request(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.StatusCode { 226 h := r.Header 227 if h.Get(requiredHeader) != "1" { 228 return safehttp.StatusPreconditionFailed 229 } 230 231 if ct := h.Get("Content-Type"); ct == "" || disallowedContentTypes[ct] { 232 return safehttp.StatusUnsupportedMediaType 233 } 234 235 if exposeHeaders := w.Header().Claim("Access-Control-Expose-Headers"); it.ExposedHeaders != nil { 236 exposeHeaders([]string{strings.Join(it.ExposedHeaders, ", ")}) 237 } 238 return 0 239 }