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  }