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 }