github.com/google/go-safeweb@v0.0.0-20231219055052-64d8cfc90fbb/safehttp/plugins/collector/collector.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 collector provides a function for creating violation report handlers. 16 // The created safehttp.Handler will be able to parse generic violation reports 17 // as specified by https://w3c.github.io/reporting/ and CSP violation reports as 18 // specified by https://www.w3.org/TR/CSP3/#deprecated-serialize-violation. 19 package collector 20 21 import ( 22 "encoding/json" 23 "io/ioutil" 24 25 "github.com/google/go-safeweb/safehttp" 26 ) 27 28 // Report represents a generic report as specified by https://w3c.github.io/reporting/#serialize-reports 29 type Report struct { 30 // Type represents the type of the report. This will control how Body looks 31 // like. 32 Type string 33 // Age represents the number of milliseconds since the violation causing the 34 // report occured. 35 Age uint64 36 // URL is the address of the Document or Worker from which the report was 37 // generated. 38 URL string 39 // UserAgent contains the value of the User-Agent header of the request from 40 // which the report was generated. 41 UserAgent string 42 // Body contains the body of the report. This will be different for every Type. 43 // If Type is csp-violation then Body will be a CSPReport. Otherwise Body will 44 // be a map[string]interface{} containing the object that was passed, as unmarshalled 45 // using encoding/json. 46 Body interface{} 47 } 48 49 // CSPReport represents a CSP violation report as specified by https://www.w3.org/TR/CSP3/#deprecated-serialize-violation 50 type CSPReport struct { 51 // BlockedURL is the URL of the resource that was blocked from loading by the 52 // Content Security Policy. If the blocked URL is from a different origin than 53 // the DocumentURL, the blocked URL is truncated to contain just the scheme, 54 // host and port. 55 BlockedURL string 56 // Disposition is either "enforce" or "report" depending on whether the Content-Security-Policy 57 // header or the Content-Security-Policy-Report-Only header is used. 58 Disposition string 59 // DocumentURL is the URL of the document in which the violation occurred. 60 DocumentURL string 61 // EffectiveDirective is the directive whose enforcement caused the violation. 62 EffectiveDirective string 63 // OriginalPolicy is the original policy as specified by the Content Security 64 // Policy header. 65 OriginalPolicy string 66 // Referrer is the referrer of the document in which the violation occurred. 67 Referrer string 68 // Sample is the first 40 characters of the inline script, event handler, 69 // or style that caused the violation. 70 Sample string 71 // StatusCode is the HTTP status code of the resource on which the global object 72 // was instantiated. 73 StatusCode uint 74 // ViolatedDirective is the name of the policy section that was violated. 75 ViolatedDirective string 76 // SourceFile represents the URL of the document or worker in which the violation 77 // was found. 78 SourceFile string 79 // LineNumber is the line number in the document or worker at which the violation 80 // occurred. 81 LineNumber uint 82 // ColumnNumber is the column number in the document or worker at which the violation 83 // occurred. 84 ColumnNumber uint 85 } 86 87 // Handler builds a safehttp.Handler which calls the given handler or cspHandler when 88 // a violation report is received. Make sure to register the handler to receive POST 89 // requests. If the handler recieves anything other than POST requests it will 90 // respond with a 405 Method Not Allowed. 91 func Handler(handler func(Report), cspHandler func(CSPReport)) safehttp.Handler { 92 return safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result { 93 if r.Method() != safehttp.MethodPost { 94 return w.WriteError(safehttp.StatusMethodNotAllowed) 95 } 96 97 b, err := ioutil.ReadAll(r.Body()) 98 if err != nil { 99 return w.WriteError(safehttp.StatusBadRequest) 100 } 101 102 ct := r.Header.Get("Content-Type") 103 if ct == "application/csp-report" { 104 return handleDeprecatedCSPReports(cspHandler, w, b) 105 } else if ct == "application/reports+json" { 106 return handleReport(handler, w, b) 107 } 108 109 return w.WriteError(safehttp.StatusUnsupportedMediaType) 110 }) 111 } 112 113 func handleDeprecatedCSPReports(h func(CSPReport), w safehttp.ResponseWriter, b []byte) safehttp.Result { 114 // In CSP2 it is clearly stated that a report has a single key 'csp-report' 115 // which holds the report object. Like this: 116 // { 117 // "csp-report": { 118 // // report goes here 119 // } 120 // } 121 // Source: https://www.w3.org/TR/CSP2/#violation-reports 122 // 123 // But in the CSP3 spec this 'csp-report' key is never mentioned. So the report 124 // would look like this: 125 // { 126 // // report goes here 127 // } 128 // Source: https://w3c.github.io/webappsec-csp/#deprecated-serialize-violation 129 // 130 // Because of this we have to support both. :/ 131 r := struct { 132 CSPReport json.RawMessage `json:"csp-report"` 133 BlockedURL string `json:"blocked-uri"` 134 Disposition string `json:"disposition"` 135 DocumentURL string `json:"document-uri"` 136 EffectiveDirectiv string `json:"effective-directive"` 137 OriginalPolicy string `json:"original-policy"` 138 Referrer string `json:"referrer"` 139 Sample string `json:"script-sample"` 140 StatusCode uint `json:"status-code"` 141 ViolatedDirective string `json:"violated-directive"` 142 SourceFile string `json:"source-file"` 143 LineNo uint `json:"lineno"` 144 LineNumber uint `json:"line-number"` 145 ColNo uint `json:"colno"` 146 ColumnNumber uint `json:"column-number"` 147 }{} 148 if err := json.Unmarshal(b, &r); err != nil { 149 return w.WriteError(safehttp.StatusBadRequest) 150 } 151 152 if len(r.CSPReport) != 0 { 153 if err := json.Unmarshal(r.CSPReport, &r); err != nil { 154 return w.WriteError(safehttp.StatusBadRequest) 155 } 156 } 157 158 ln := r.LineNo 159 if ln == 0 { 160 ln = r.LineNumber 161 } 162 cn := r.ColNo 163 if cn == 0 { 164 cn = r.ColumnNumber 165 } 166 167 rr := CSPReport{ 168 BlockedURL: r.BlockedURL, 169 Disposition: r.Disposition, 170 DocumentURL: r.DocumentURL, 171 EffectiveDirective: r.EffectiveDirectiv, 172 OriginalPolicy: r.OriginalPolicy, 173 Referrer: r.Referrer, 174 Sample: r.Sample, 175 StatusCode: r.StatusCode, 176 ViolatedDirective: r.ViolatedDirective, 177 SourceFile: r.SourceFile, 178 LineNumber: ln, 179 ColumnNumber: cn, 180 } 181 h(rr) 182 183 return w.Write(safehttp.NoContentResponse{}) 184 } 185 186 var reportHandlers = map[string]func(json.RawMessage) (body interface{}, ok bool){ 187 "csp-violation": cspViolationHandler, 188 } 189 190 func handleReport(h func(Report), w safehttp.ResponseWriter, b []byte) safehttp.Result { 191 var rList []struct { 192 Type string `json:"type"` 193 Age uint64 `json:"age"` 194 URL string `json:"url"` 195 UserAgent string `json:"userAgent"` 196 Body json.RawMessage `json:"body"` 197 } 198 if err := json.Unmarshal(b, &rList); err != nil { 199 return w.WriteError(safehttp.StatusBadRequest) 200 } 201 202 badReport := false 203 for _, r := range rList { 204 reportToSend := Report{ 205 Type: r.Type, 206 Age: r.Age, 207 URL: r.URL, 208 UserAgent: r.UserAgent, 209 } 210 211 if f, ok := reportHandlers[r.Type]; ok { 212 b, ok := f(r.Body) 213 if !ok { 214 badReport = true 215 continue 216 } 217 reportToSend.Body = b 218 } else { 219 b := map[string]interface{}{} 220 if err := json.Unmarshal(r.Body, &b); err != nil { 221 badReport = true 222 continue 223 } 224 reportToSend.Body = b 225 } 226 227 h(reportToSend) 228 } 229 230 if badReport { 231 return w.WriteError(safehttp.StatusBadRequest) 232 } 233 234 return w.Write(safehttp.NoContentResponse{}) 235 } 236 237 // cspViolationHandler parses reports of type csp-violation and returns a CSPReport. 238 func cspViolationHandler(m json.RawMessage) (body interface{}, ok bool) { 239 // https://w3c.github.io/webappsec-csp/#reporting 240 r := struct { 241 BlockedURL string `json:"blockedURL"` 242 Disposition string `json:"disposition"` 243 DocumentURL string `json:"documentURL"` 244 EffectiveDirective string `json:"effectiveDirective"` 245 OriginalPolicy string `json:"originalPolicy"` 246 Referrer string `json:"referrer"` 247 Sample string `json:"sample"` 248 StatusCode uint `json:"statusCode"` 249 SourceFile string `json:"sourceFile"` 250 LineNumber uint `json:"lineNumber"` 251 ColumnNumber uint `json:"columnNumber"` 252 }{} 253 if err := json.Unmarshal(m, &r); err != nil { 254 return nil, false 255 } 256 257 return CSPReport{ 258 BlockedURL: r.BlockedURL, 259 Disposition: r.Disposition, 260 DocumentURL: r.DocumentURL, 261 EffectiveDirective: r.EffectiveDirective, 262 OriginalPolicy: r.OriginalPolicy, 263 Referrer: r.Referrer, 264 Sample: r.Sample, 265 StatusCode: r.StatusCode, 266 // In CSP3 ViolatedDirective has been removed but is kept as 267 // a copy of EffectiveDirective for backwards compatibility. 268 ViolatedDirective: r.EffectiveDirective, 269 SourceFile: r.SourceFile, 270 LineNumber: r.LineNumber, 271 ColumnNumber: r.ColumnNumber, 272 }, true 273 }