github.com/google/go-safeweb@v0.0.0-20231219055052-64d8cfc90fbb/safehttp/plugins/csp/csp_all_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 csp
    16  
    17  import (
    18  	"os"
    19  	"testing"
    20  
    21  	"github.com/google/go-cmp/cmp"
    22  	"github.com/google/go-cmp/cmp/cmpopts"
    23  	"github.com/google/go-safeweb/safehttp"
    24  	"github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp"
    25  	"github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp/unsafecspfortests"
    26  	"github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp/unsafestrictcsp"
    27  	"github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp/unsafetrustedtypes"
    28  	"github.com/google/go-safeweb/safehttp/plugins/framing/internalunsafeframing"
    29  	"github.com/google/go-safeweb/safehttp/plugins/framing/internalunsafeframing/unsafeframing"
    30  	"github.com/google/go-safeweb/safehttp/safehttptest"
    31  )
    32  
    33  func TestMain(m *testing.M) {
    34  	unsafecspfortests.UseStaticRandom()
    35  	os.Exit(m.Run())
    36  }
    37  
    38  func TestSerialize(t *testing.T) {
    39  	tests := []struct {
    40  		name       string
    41  		policy     Policy
    42  		wantString string
    43  	}{
    44  		{
    45  			name:       "StrictCSP",
    46  			policy:     StrictPolicy{},
    47  			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret' 'strict-dynamic' https: http:; base-uri 'none'",
    48  		},
    49  		{
    50  			name:       "StrictCSP with no strict-dynamic",
    51  			policy:     StrictPolicy{NoStrictDynamic: true},
    52  			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret'; base-uri 'none'",
    53  		},
    54  		{
    55  			name:       "StrictCSP with unsafe-eval",
    56  			policy:     StrictPolicy{UnsafeEval: true},
    57  			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret' 'strict-dynamic' https: http: 'unsafe-eval'; base-uri 'none'",
    58  		},
    59  		{
    60  			name:       "StrictCSP with set base-uri",
    61  			policy:     StrictPolicy{BaseURI: "https://example.com"},
    62  			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret' 'strict-dynamic' https: http:; base-uri https://example.com",
    63  		},
    64  		{
    65  			name:       "StrictCSP with report-uri",
    66  			policy:     StrictPolicy{ReportURI: "https://example.com/collector"},
    67  			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret' 'strict-dynamic' https: http:; base-uri 'none'; report-uri https://example.com/collector",
    68  		},
    69  		{
    70  			name: "StrictCSP with one hash",
    71  			policy: StrictPolicy{Hashes: []string{
    72  				"sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M=",
    73  			}},
    74  			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret' 'strict-dynamic' https: http: 'sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M='; base-uri 'none'",
    75  		},
    76  		{
    77  			name: "StrictCSP with multiple hashes",
    78  			policy: StrictPolicy{Hashes: []string{
    79  				"sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M=",
    80  				"sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M=",
    81  			}},
    82  			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret' 'strict-dynamic' https: http: 'sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M=' 'sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M='; base-uri 'none'",
    83  		},
    84  		{
    85  			name:       "FramingCSP",
    86  			policy:     FramingPolicy{},
    87  			wantString: "frame-ancestors 'self';",
    88  		},
    89  		{
    90  			name:       "FramingCSP with report-uri",
    91  			policy:     FramingPolicy{ReportURI: "httsp://example.com/collector"},
    92  			wantString: "frame-ancestors 'self'; report-uri httsp://example.com/collector;",
    93  		},
    94  		{
    95  			name:       "TrustedTypesCSP",
    96  			policy:     TrustedTypesPolicy{},
    97  			wantString: "require-trusted-types-for 'script'",
    98  		},
    99  		{
   100  			name:       "TrustedTypesCSP with report-uri",
   101  			policy:     TrustedTypesPolicy{ReportURI: "httsp://example.com/collector"},
   102  			wantString: "require-trusted-types-for 'script'; report-uri httsp://example.com/collector",
   103  		},
   104  	}
   105  
   106  	for _, tt := range tests {
   107  		t.Run(tt.name, func(t *testing.T) {
   108  			s := tt.policy.Serialize("super-secret", nil)
   109  
   110  			if s != tt.wantString {
   111  				t.Errorf("tt.policy.Serialize() got: %q want: %q", s, tt.wantString)
   112  			}
   113  		})
   114  	}
   115  }
   116  
   117  func TestBefore(t *testing.T) {
   118  	tests := []struct {
   119  		name                 string
   120  		interceptors         []Interceptor
   121  		wantEnforcePolicy    []string
   122  		wantReportOnlyPolicy []string
   123  		wantNonce            string
   124  	}{
   125  		{
   126  			name:         "Default policies",
   127  			interceptors: Default(""),
   128  			wantEnforcePolicy: []string{
   129  				"object-src 'none'; script-src 'unsafe-inline' 'nonce-KSkpKSkpKSkpKSkpKSkpKSkpKSk=' 'strict-dynamic' https: http:; base-uri 'none'",
   130  				"require-trusted-types-for 'script'",
   131  			},
   132  			wantNonce: "KSkpKSkpKSkpKSkpKSkpKSkpKSk=",
   133  		},
   134  		{
   135  			name:         "All policies",
   136  			interceptors: append(Default(""), Interceptor{Policy: FramingPolicy{}}),
   137  			wantEnforcePolicy: []string{
   138  				"object-src 'none'; script-src 'unsafe-inline' 'nonce-KSkpKSkpKSkpKSkpKSkpKSkpKSk=' 'strict-dynamic' https: http:; base-uri 'none'",
   139  				"require-trusted-types-for 'script'",
   140  				"frame-ancestors 'self';",
   141  			},
   142  			wantNonce: "KSkpKSkpKSkpKSkpKSkpKSkpKSk=",
   143  		},
   144  		{
   145  			name: "All policies with reporting URI",
   146  			interceptors: append(Default("https://example.com/collector"),
   147  				Interceptor{Policy: FramingPolicy{ReportURI: "https://example.com/collector"}}),
   148  			wantEnforcePolicy: []string{
   149  				"object-src 'none'; script-src 'unsafe-inline' 'nonce-KSkpKSkpKSkpKSkpKSkpKSkpKSk=' 'strict-dynamic' https: http:; base-uri 'none'; report-uri https://example.com/collector",
   150  				"require-trusted-types-for 'script'; report-uri https://example.com/collector",
   151  				"frame-ancestors 'self'; report-uri https://example.com/collector;",
   152  			},
   153  			wantNonce: "KSkpKSkpKSkpKSkpKSkpKSkpKSk=",
   154  		},
   155  		{
   156  			name: "StrictCSP Report Only",
   157  			interceptors: []Interceptor{{
   158  				Policy:     StrictPolicy{ReportURI: "https://example.com/collector"},
   159  				ReportOnly: true,
   160  			}},
   161  			wantReportOnlyPolicy: []string{
   162  				"object-src 'none'; script-src 'unsafe-inline' 'nonce-KSkpKSkpKSkpKSkpKSkpKSkpKSk=' 'strict-dynamic' https: http:; base-uri 'none'; report-uri https://example.com/collector",
   163  			},
   164  			wantNonce: "KSkpKSkpKSkpKSkpKSkpKSkpKSk=",
   165  		},
   166  		{
   167  			name: "FramingCSP Report Only",
   168  			interceptors: []Interceptor{{
   169  				Policy:     FramingPolicy{ReportURI: "https://example.com/collector"},
   170  				ReportOnly: true,
   171  			}},
   172  			wantReportOnlyPolicy: []string{"frame-ancestors 'self'; report-uri https://example.com/collector;"},
   173  			wantNonce:            "KSkpKSkpKSkpKSkpKSkpKSkpKSk=",
   174  		},
   175  	}
   176  
   177  	for _, tt := range tests {
   178  		t.Run(tt.name, func(t *testing.T) {
   179  			fakeRW, rr := safehttptest.NewFakeResponseWriter()
   180  			req := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
   181  
   182  			for _, i := range tt.interceptors {
   183  				i.Before(fakeRW, req, nil)
   184  			}
   185  
   186  			h := rr.Header()
   187  			if diff := cmp.Diff(tt.wantEnforcePolicy, h.Values("Content-Security-Policy"), cmpopts.EquateEmpty()); diff != "" {
   188  				t.Errorf("h.Values(\"Content-Security-Policy\") mismatch (-want +got):\n%s", diff)
   189  			}
   190  
   191  			if diff := cmp.Diff(tt.wantReportOnlyPolicy, h.Values("Content-Security-Policy-Report-Only"), cmpopts.EquateEmpty()); diff != "" {
   192  				t.Errorf("h.Values(\"Content-Security-Policy-Report-Only\") mismatch (-want +got):\n%s", diff)
   193  			}
   194  
   195  			v := safehttp.FlightValues(req.Context()).Get(nonceKey)
   196  			if v == nil {
   197  				t.Fatalf("safehttp.FlightValues(req.Context()).Get(nonceCtxKey) got: nil want: %q", tt.wantNonce)
   198  			}
   199  			if got := v.(string); got != tt.wantNonce {
   200  				t.Errorf("v.(string) got: %q want: %q", got, tt.wantNonce)
   201  			}
   202  		})
   203  	}
   204  }
   205  
   206  func TestValidNonce(t *testing.T) {
   207  	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
   208  	_ = nonce(req)
   209  
   210  	n, err := Nonce(req.Context())
   211  	if err != nil {
   212  		t.Errorf("Nonce(ctx) got err: %v want: nil", err)
   213  	}
   214  
   215  	if want := "KSkpKSkpKSkpKSkpKSkpKSkpKSk="; n != want {
   216  		t.Errorf("Nonce(ctx) got nonce: %v want: %v", n, want)
   217  	}
   218  }
   219  
   220  func TestNonceEmptyContext(t *testing.T) {
   221  	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
   222  	// Not using nonce() to insert the nonce in context.
   223  
   224  	n, err := Nonce(req.Context())
   225  	if err == nil {
   226  		t.Error("Nonce(ctx) got err: nil want: error")
   227  	}
   228  
   229  	if want := ""; n != want {
   230  		t.Errorf("Nonce(ctx) got nonce: %v want: %v", n, want)
   231  	}
   232  }
   233  
   234  func TestCommitNonce(t *testing.T) {
   235  	fakeRW, rr := safehttptest.NewFakeResponseWriter()
   236  	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
   237  	safehttp.FlightValues(req.Context()).Put(nonceKey, "pizza")
   238  
   239  	it := Interceptor{}
   240  	tr := &safehttp.TemplateResponse{}
   241  	it.Commit(fakeRW, req, tr, nil)
   242  
   243  	nonce, ok := tr.FuncMap["CSPNonce"]
   244  	if !ok {
   245  		t.Fatal(`tr.FuncMap["CSPNonce"] not found`)
   246  	}
   247  
   248  	fn, ok := nonce.(func() string)
   249  	if !ok {
   250  		t.Fatalf(`tr.FuncMap["CSPNonce"]: got %T, want "func() string"`, fn)
   251  	}
   252  	if got, want := fn(), "pizza"; want != got {
   253  		t.Errorf(`tr.FuncMap["CSPNonce"](): got %q, want %q`, got, want)
   254  	}
   255  
   256  	if got, want := rr.Code, int(safehttp.StatusOK); got != want {
   257  		t.Errorf("rr.Code: got %v, want %v", got, want)
   258  	}
   259  
   260  	if diff := cmp.Diff(map[string][]string{}, map[string][]string(rr.Header())); diff != "" {
   261  		t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
   262  	}
   263  
   264  	if got, want := "", rr.Body.String(); got != want {
   265  		t.Errorf("rr.Body.String(): got %q want %q", got, want)
   266  	}
   267  }
   268  
   269  func TestCommitMissingNonce(t *testing.T) {
   270  	fakeRW, _ := safehttptest.NewFakeResponseWriter()
   271  	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
   272  	// Not adding safehttp.FlightValues here.
   273  
   274  	it := Interceptor{}
   275  	tr := &safehttp.TemplateResponse{}
   276  
   277  	defer func() {
   278  		if r := recover(); r == nil {
   279  			t.Fatal("expected panic")
   280  		}
   281  	}()
   282  	it.Commit(fakeRW, req, tr, nil)
   283  }
   284  
   285  func TestCommitNotTemplateResponse(t *testing.T) {
   286  	fakeRW, rr := safehttptest.NewFakeResponseWriter()
   287  	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
   288  
   289  	it := Interceptor{}
   290  	it.Commit(fakeRW, req, safehttp.NoContentResponse{}, nil)
   291  
   292  	if got, want := rr.Code, int(safehttp.StatusOK); got != want {
   293  		t.Errorf("rr.Code: got %v, want %v", got, want)
   294  	}
   295  
   296  	if diff := cmp.Diff(map[string][]string{}, map[string][]string(rr.Header())); diff != "" {
   297  		t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
   298  	}
   299  
   300  	if got, want := rr.Body.String(), ""; got != want {
   301  		t.Errorf("rr.Body.String(): got %q want %q", got, want)
   302  	}
   303  
   304  }
   305  
   306  func TestOverride(t *testing.T) {
   307  	tests := []struct {
   308  		name                 string
   309  		interceptors         []Interceptor
   310  		overrides            []safehttp.InterceptorConfig
   311  		wantEnforcePolicy    []string
   312  		wantReportOnlyPolicy []string
   313  	}{
   314  		{
   315  			name:         "All policies, completely disabled",
   316  			interceptors: append(Default(""), Interceptor{Policy: FramingPolicy{}}),
   317  			overrides: []safehttp.InterceptorConfig{
   318  				internalunsafecsp.DisableStrict{SkipReports: true},
   319  				internalunsafecsp.DisableTrustedTypes{SkipReports: true},
   320  				internalunsafeframing.Disable{SkipReports: true},
   321  			},
   322  		},
   323  		{
   324  			name:         "All policies, disabled via unsafe packages",
   325  			interceptors: append(Default(""), Interceptor{Policy: FramingPolicy{}}),
   326  			overrides: []safehttp.InterceptorConfig{
   327  				unsafestrictcsp.Disable("testing", true),
   328  				unsafetrustedtypes.Disable("testing", true),
   329  				unsafeframing.Disable("testing", true),
   330  			},
   331  		},
   332  		{
   333  			name:         "All policies, report-only override",
   334  			interceptors: append(Default(""), Interceptor{Policy: FramingPolicy{}}),
   335  			overrides: []safehttp.InterceptorConfig{
   336  				internalunsafecsp.DisableStrict{},
   337  				internalunsafecsp.DisableTrustedTypes{},
   338  				internalunsafeframing.Disable{},
   339  			},
   340  			wantReportOnlyPolicy: []string{
   341  				"object-src 'none'; script-src 'unsafe-inline' 'nonce-KSkpKSkpKSkpKSkpKSkpKSkpKSk=' 'strict-dynamic' https: http:; base-uri 'none'",
   342  				"require-trusted-types-for 'script'",
   343  				"frame-ancestors 'self';",
   344  			},
   345  		},
   346  		{
   347  			name: "FramingCSP allowlist",
   348  			interceptors: []Interceptor{Interceptor{
   349  				Policy: FramingPolicy{}}},
   350  			overrides: []safehttp.InterceptorConfig{
   351  				unsafeframing.Allow("testing", true, "https://www.example.org"),
   352  			},
   353  			wantReportOnlyPolicy: []string{"frame-ancestors 'self' https://www.example.org;"},
   354  		},
   355  		{
   356  			name: "FramingCSP allowlist",
   357  			interceptors: []Interceptor{Interceptor{
   358  				Policy: FramingPolicy{}}},
   359  			overrides: []safehttp.InterceptorConfig{
   360  				unsafeframing.Allow("testing", false, "https://a.example.org", "https://b.example.org"),
   361  			},
   362  			wantEnforcePolicy: []string{
   363  				"frame-ancestors 'self' https://a.example.org https://b.example.org;"},
   364  		},
   365  	}
   366  
   367  	for _, tt := range tests {
   368  		t.Run(tt.name, func(t *testing.T) {
   369  			fakeRW, rr := safehttptest.NewFakeResponseWriter()
   370  			req := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
   371  
   372  			for _, i := range tt.interceptors {
   373  				var cfg safehttp.InterceptorConfig
   374  				for _, c := range tt.overrides {
   375  					if i.Match(c) {
   376  						if cfg != nil {
   377  							t.Fatalf("Multiple overrides match: %v and %v", cfg, c)
   378  						}
   379  						cfg = c
   380  					}
   381  				}
   382  				i.Before(fakeRW, req, cfg)
   383  			}
   384  
   385  			h := rr.Header()
   386  			if diff := cmp.Diff(tt.wantEnforcePolicy, h.Values("Content-Security-Policy"), cmpopts.EquateEmpty()); diff != "" {
   387  				t.Errorf("h.Values(\"Content-Security-Policy\") mismatch (-want +got):\n%s", diff)
   388  			}
   389  
   390  			if diff := cmp.Diff(tt.wantReportOnlyPolicy, h.Values("Content-Security-Policy-Report-Only"), cmpopts.EquateEmpty()); diff != "" {
   391  				t.Errorf("h.Values(\"Content-Security-Policy-Report-Only\") mismatch (-want +got):\n%s", diff)
   392  			}
   393  		})
   394  	}
   395  }