github.com/google/go-safeweb@v0.0.0-20231219055052-64d8cfc90fbb/safehttp/plugins/collector/collector_test.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_test 16 17 import ( 18 "strings" 19 "testing" 20 21 "github.com/google/go-cmp/cmp" 22 "github.com/google/go-safeweb/safehttp" 23 "github.com/google/go-safeweb/safehttp/plugins/collector" 24 "github.com/google/go-safeweb/safehttp/safehttptest" 25 ) 26 27 func TestValidReport(t *testing.T) { 28 tests := []struct { 29 name string 30 report string 31 want []collector.Report 32 }{ 33 { 34 name: "Custom report", 35 report: `[{ 36 "type": "custom", 37 "age": 10, 38 "url": "https://example.com/vulnerable-page/", 39 "userAgent": "chrome", 40 "body": { 41 "x": "y", 42 "pizza": "hawaii", 43 "roundness": 3.14 44 } 45 }]`, 46 want: []collector.Report{ 47 { 48 Type: "custom", 49 Age: 10, 50 URL: "https://example.com/vulnerable-page/", 51 UserAgent: "chrome", 52 Body: map[string]interface{}{ 53 "x": "y", 54 "pizza": "hawaii", 55 "roundness": float64(3.14), 56 }, 57 }, 58 }, 59 }, 60 { 61 name: "Multiple reports", 62 report: `[{ 63 "type": "custom", 64 "age": 10, 65 "url": "https://example.com/vulnerable-page/", 66 "userAgent": "chrome", 67 "body": { 68 "x": "y", 69 "pizza": "hawaii", 70 "roundness": 3.14 71 } 72 }, 73 { 74 "type": "custom", 75 "age": 15, 76 "url": "https://example.com/", 77 "userAgent": "firefox", 78 "body": { 79 "x": "z", 80 "pizza": "kebab", 81 "roundness": 1.234 82 } 83 }]`, 84 want: []collector.Report{ 85 { 86 Type: "custom", 87 Age: 10, 88 URL: "https://example.com/vulnerable-page/", 89 UserAgent: "chrome", 90 Body: map[string]interface{}{ 91 "x": "y", 92 "pizza": "hawaii", 93 "roundness": float64(3.14), 94 }, 95 }, 96 { 97 Type: "custom", 98 Age: 15, 99 URL: "https://example.com/", 100 UserAgent: "firefox", 101 Body: map[string]interface{}{ 102 "x": "z", 103 "pizza": "kebab", 104 "roundness": float64(1.234), 105 }, 106 }, 107 }, 108 }, 109 { 110 name: "csp-violation", 111 report: `[{ 112 "type": "csp-violation", 113 "age": 10, 114 "url": "https://example.com/vulnerable-page/", 115 "userAgent": "chrome", 116 "body": { 117 "blockedURL": "https://evil.com/", 118 "disposition": "enforce", 119 "documentURL": "https://example.com/blah/blah", 120 "effectiveDirective": "script-src sha256.eZcc0TlUfnQi64XLdiN5c/Vh2vtDbPaGtXRFyE7dRLo=", 121 "originalPolicy": "object-src 'none'; script-src sha256.eZcc0TlUfnQi64XLdiN5c/Vh2vtDbPaGtXRFyE7dRLo=", 122 "referrer": "https://example.com/", 123 "sample": "alert(1)", 124 "statusCode": 200, 125 "sourceFile": "stuff.js", 126 "lineNumber": 10, 127 "columnNumber": 17 128 } 129 }]`, 130 want: []collector.Report{ 131 { 132 Type: "csp-violation", 133 Age: 10, 134 URL: "https://example.com/vulnerable-page/", 135 UserAgent: "chrome", 136 Body: collector.CSPReport{ 137 BlockedURL: "https://evil.com/", 138 Disposition: "enforce", 139 DocumentURL: "https://example.com/blah/blah", 140 EffectiveDirective: "script-src sha256.eZcc0TlUfnQi64XLdiN5c/Vh2vtDbPaGtXRFyE7dRLo=", 141 OriginalPolicy: "object-src 'none'; script-src sha256.eZcc0TlUfnQi64XLdiN5c/Vh2vtDbPaGtXRFyE7dRLo=", 142 Referrer: "https://example.com/", 143 Sample: "alert(1)", 144 StatusCode: 200, 145 ViolatedDirective: "script-src sha256.eZcc0TlUfnQi64XLdiN5c/Vh2vtDbPaGtXRFyE7dRLo=", 146 SourceFile: "stuff.js", 147 LineNumber: 10, 148 ColumnNumber: 17, 149 }, 150 }, 151 }, 152 }, 153 } 154 155 for _, tt := range tests { 156 t.Run(tt.name, func(t *testing.T) { 157 var gotReports []collector.Report 158 h := collector.Handler(func(r collector.Report) { 159 gotReports = append(gotReports, r) 160 }, func(r collector.CSPReport) { 161 t.Fatalf("expected CSP reports handler not to be called") 162 }) 163 164 req := safehttptest.NewRequest(safehttp.MethodPost, "/collector", strings.NewReader(tt.report)) 165 req.Header.Set("Content-Type", "application/reports+json") 166 167 fakeRW, rr := safehttptest.NewFakeResponseWriter() 168 h.ServeHTTP(fakeRW, req) 169 170 if diff := cmp.Diff(tt.want, gotReports); diff != "" { 171 t.Errorf("reports gotten mismatch (-want +got):\n%s", diff) 172 } 173 174 if got, want := rr.Code, int(safehttp.StatusNoContent); got != want { 175 t.Errorf("rr.Code got: %v want: %v", got, want) 176 } 177 if diff := cmp.Diff(map[string][]string{}, map[string][]string(rr.Header())); diff != "" { 178 t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff) 179 } 180 if got, want := rr.Body.String(), ""; got != want { 181 t.Errorf("rr.Body() got: %q want: %q", got, want) 182 } 183 }) 184 } 185 } 186 187 func TestValidDeprecatedCSPReport(t *testing.T) { 188 tests := []struct { 189 name string 190 report string 191 want collector.CSPReport 192 }{ 193 { 194 name: "Basic", 195 report: `{ 196 "csp-report": { 197 "blocked-uri": "https://evil.com/", 198 "disposition": "enforce", 199 "document-uri": "https://example.com/blah/blah", 200 "effective-directive": "script-src sha256.eZcc0TlUfnQi64XLdiN5c/Vh2vtDbPaGtXRFyE7dRLo=", 201 "original-policy": "object-src 'none'; script-src sha256.eZcc0TlUfnQi64XLdiN5c/Vh2vtDbPaGtXRFyE7dRLo=", 202 "referrer": "https://example.com/", 203 "script-sample": "alert(1)", 204 "status-code": 200, 205 "violated-directive": "script-src", 206 "source-file": "stuff.js" 207 } 208 }`, 209 want: collector.CSPReport{ 210 BlockedURL: "https://evil.com/", 211 Disposition: "enforce", 212 DocumentURL: "https://example.com/blah/blah", 213 EffectiveDirective: "script-src sha256.eZcc0TlUfnQi64XLdiN5c/Vh2vtDbPaGtXRFyE7dRLo=", 214 OriginalPolicy: "object-src 'none'; script-src sha256.eZcc0TlUfnQi64XLdiN5c/Vh2vtDbPaGtXRFyE7dRLo=", 215 Referrer: "https://example.com/", 216 Sample: "alert(1)", 217 StatusCode: 200, 218 ViolatedDirective: "script-src", 219 SourceFile: "stuff.js", 220 }, 221 }, 222 { 223 name: "No csp-report key", 224 report: `{ 225 "blocked-uri": "https://evil.com/", 226 "disposition": "enforce", 227 "document-uri": "https://example.com/blah/blah", 228 "effective-directive": "script-src sha256.eZcc0TlUfnQi64XLdiN5c/Vh2vtDbPaGtXRFyE7dRLo=", 229 "original-policy": "object-src 'none'; script-src sha256.eZcc0TlUfnQi64XLdiN5c/Vh2vtDbPaGtXRFyE7dRLo=", 230 "referrer": "https://example.com/", 231 "script-sample": "alert(1)", 232 "status-code": 200, 233 "violated-directive": "script-src", 234 "source-file": "stuff.js" 235 }`, 236 want: collector.CSPReport{ 237 BlockedURL: "https://evil.com/", 238 Disposition: "enforce", 239 DocumentURL: "https://example.com/blah/blah", 240 EffectiveDirective: "script-src sha256.eZcc0TlUfnQi64XLdiN5c/Vh2vtDbPaGtXRFyE7dRLo=", 241 OriginalPolicy: "object-src 'none'; script-src sha256.eZcc0TlUfnQi64XLdiN5c/Vh2vtDbPaGtXRFyE7dRLo=", 242 Referrer: "https://example.com/", 243 Sample: "alert(1)", 244 StatusCode: 200, 245 ViolatedDirective: "script-src", 246 SourceFile: "stuff.js", 247 }, 248 }, 249 { 250 name: "lineno and colno", 251 report: `{ 252 "csp-report": { 253 "lineno": 15, 254 "colno": 10 255 } 256 }`, 257 want: collector.CSPReport{ 258 LineNumber: 15, 259 ColumnNumber: 10, 260 }, 261 }, 262 { 263 name: "line-number and column-number", 264 report: `{ 265 "csp-report": { 266 "line-number": 15, 267 "column-number": 10 268 } 269 }`, 270 want: collector.CSPReport{ 271 LineNumber: 15, 272 ColumnNumber: 10, 273 }, 274 }, 275 { 276 name: "Both lineno and colno, and line-number and column-number", 277 report: `{ 278 "csp-report": { 279 "lineno": 7, 280 "colno": 8, 281 "line-number": 15, 282 "column-number": 10 283 } 284 }`, 285 want: collector.CSPReport{ 286 LineNumber: 7, 287 ColumnNumber: 8, 288 }, 289 }, 290 } 291 292 for _, tt := range tests { 293 t.Run(tt.name, func(t *testing.T) { 294 h := collector.Handler(func(r collector.Report) { 295 t.Fatalf("expected generic reports handler not to be called") 296 }, func(r collector.CSPReport) { 297 if diff := cmp.Diff(tt.want, r); diff != "" { 298 t.Errorf("report mismatch (-want +got):\n%s", diff) 299 } 300 }) 301 302 req := safehttptest.NewRequest(safehttp.MethodPost, "/collector", strings.NewReader(tt.report)) 303 req.Header.Set("Content-Type", "application/csp-report") 304 305 fakeRW, rr := safehttptest.NewFakeResponseWriter() 306 h.ServeHTTP(fakeRW, req) 307 308 if got, want := rr.Code, int(safehttp.StatusNoContent); got != want { 309 t.Errorf("rr.Code got: %v want: %v", got, want) 310 } 311 if diff := cmp.Diff(map[string][]string{}, map[string][]string(rr.Header())); diff != "" { 312 t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff) 313 } 314 if got, want := rr.Body.String(), ""; got != want { 315 t.Errorf("rr.Body() got: %q want: %q", got, want) 316 } 317 }) 318 } 319 } 320 321 func TestInvalidRequest(t *testing.T) { 322 tests := []struct { 323 name string 324 req *safehttp.IncomingRequest 325 wantStatus safehttp.StatusCode 326 wantHeaders map[string][]string 327 wantBody string 328 }{ 329 { 330 name: "Method", 331 req: safehttptest.NewRequest(safehttp.MethodGet, "/collector", nil), 332 wantStatus: safehttp.StatusMethodNotAllowed, 333 wantHeaders: map[string][]string{ 334 "Content-Type": {"text/plain; charset=utf-8"}, 335 "X-Content-Type-Options": {"nosniff"}, 336 }, 337 wantBody: "Method Not Allowed\n", 338 }, 339 { 340 name: "Content-Type", 341 req: func() *safehttp.IncomingRequest { 342 r := safehttptest.NewRequest(safehttp.MethodPost, "/collector", nil) 343 r.Header.Set("Content-Type", "text/plain") 344 return r 345 }(), 346 wantStatus: safehttp.StatusUnsupportedMediaType, 347 wantHeaders: map[string][]string{ 348 "Content-Type": {"text/plain; charset=utf-8"}, 349 "X-Content-Type-Options": {"nosniff"}, 350 }, 351 wantBody: "Unsupported Media Type\n", 352 }, 353 { 354 name: "csp-report, invalid json", 355 req: func() *safehttp.IncomingRequest { 356 r := safehttptest.NewRequest(safehttp.MethodPost, "/collector", strings.NewReader(`{"a:"b"}`)) 357 r.Header.Set("Content-Type", "application/csp-report") 358 return r 359 }(), 360 wantStatus: safehttp.StatusBadRequest, 361 wantHeaders: map[string][]string{ 362 "Content-Type": {"text/plain; charset=utf-8"}, 363 "X-Content-Type-Options": {"nosniff"}, 364 }, 365 wantBody: "Bad Request\n", 366 }, 367 { 368 name: "reports+json, invalid json", 369 req: func() *safehttp.IncomingRequest { 370 r := safehttptest.NewRequest(safehttp.MethodPost, "/collector", strings.NewReader(`[{"a:"b"}]`)) 371 r.Header.Set("Content-Type", "application/reports+json") 372 return r 373 }(), 374 wantStatus: safehttp.StatusBadRequest, 375 wantHeaders: map[string][]string{ 376 "Content-Type": {"text/plain; charset=utf-8"}, 377 "X-Content-Type-Options": {"nosniff"}, 378 }, 379 wantBody: "Bad Request\n", 380 }, 381 { 382 name: "csp-report, valid json, csp-report is not an object", 383 req: func() *safehttp.IncomingRequest { 384 r := safehttptest.NewRequest(safehttp.MethodPost, "/collector", strings.NewReader(`{"csp-report":"b"}`)) 385 r.Header.Set("Content-Type", "application/csp-report") 386 return r 387 }(), 388 wantStatus: safehttp.StatusBadRequest, 389 wantHeaders: map[string][]string{ 390 "Content-Type": {"text/plain; charset=utf-8"}, 391 "X-Content-Type-Options": {"nosniff"}, 392 }, 393 wantBody: "Bad Request\n", 394 }, 395 { 396 name: "reports+json, valid json, body is not an object", 397 req: func() *safehttp.IncomingRequest { 398 r := safehttptest.NewRequest(safehttp.MethodPost, "/collector", strings.NewReader(`[{ 399 "type": "xyz", 400 "age": 10, 401 "url": "https://example.com/", 402 "userAgent": "chrome", 403 "body": "not an object" 404 }]`)) 405 r.Header.Set("Content-Type", "application/reports+json") 406 return r 407 }(), 408 wantStatus: safehttp.StatusBadRequest, 409 wantHeaders: map[string][]string{ 410 "Content-Type": {"text/plain; charset=utf-8"}, 411 "X-Content-Type-Options": {"nosniff"}, 412 }, 413 wantBody: "Bad Request\n", 414 }, 415 { 416 name: "Negative uints", 417 req: func() *safehttp.IncomingRequest { 418 r := safehttptest.NewRequest(safehttp.MethodPost, "/collector", strings.NewReader(`{ 419 "csp-report": { 420 "status-code": -1, 421 "lineno": -1, 422 "colno": -1, 423 "line-number": -1, 424 "column-number": -1 425 } 426 }`)) 427 r.Header.Set("Content-Type", "application/csp-report") 428 return r 429 }(), 430 wantStatus: safehttp.StatusBadRequest, 431 wantHeaders: map[string][]string{ 432 "Content-Type": {"text/plain; charset=utf-8"}, 433 "X-Content-Type-Options": {"nosniff"}, 434 }, 435 wantBody: "Bad Request\n", 436 }, 437 } 438 439 for _, tt := range tests { 440 t.Run(tt.name, func(t *testing.T) { 441 h := collector.Handler(func(r collector.Report) { 442 t.Errorf("expected collector not to be called") 443 }, func(r collector.CSPReport) { 444 t.Errorf("expected collector not to be called") 445 }) 446 447 fakeRW, rr := safehttptest.NewFakeResponseWriter() 448 h.ServeHTTP(fakeRW, tt.req) 449 450 if got, want := rr.Code, int(tt.wantStatus); got != want { 451 t.Errorf("rr.Code got: %v want: %v", got, want) 452 } 453 if diff := cmp.Diff(map[string][]string{}, map[string][]string(rr.Header())); diff != "" { 454 t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff) 455 } 456 }) 457 } 458 }