test(19.3-06): unit tests for the review fixes
- TestCORSCredentialedOriginRules, TestCORSAnchoredPatternNoSuffixMatch (D-04) - TestAPIDocsCSPNoHostSource (D-17) - TestLoadCookieConfigProductionInferredSecure, TestZeroCookieConfigDefaults (D-03) - TestSiteCachePurgeWaitAndClose, TestSiteCacheBulkCommitCoalesced (D-10)
This commit is contained in:
@@ -305,3 +305,67 @@ func TestAPIDocsTitleEscaped(t *testing.T) {
|
||||
t.Fatal("default title missing")
|
||||
}
|
||||
}
|
||||
|
||||
// cspDirectives splits a Content-Security-Policy value into its directives.
|
||||
func cspDirectives(csp string) map[string][]string {
|
||||
out := map[string][]string{}
|
||||
for _, part := range strings.Split(csp, ";") {
|
||||
fields := strings.Fields(part)
|
||||
if len(fields) == 0 {
|
||||
continue
|
||||
}
|
||||
out[fields[0]] = fields[1:]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestAPIDocsCSPNoHostSource(t *testing.T) {
|
||||
h, err := Assemble(apiDocsApp(t, "api_docs:\n enabled: true\n"), []party.Plugin{specPlugin{id: "acme.zoo", spec: orderedSpecYAML}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nonces := map[string]bool{}
|
||||
for range 2 {
|
||||
rec := apiGet(h, "/api-docs")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("GET /api-docs = %d", rec.Code)
|
||||
}
|
||||
dirs := cspDirectives(rec.Header().Get("Content-Security-Policy"))
|
||||
script := dirs["script-src"]
|
||||
if len(script) != 1 || !strings.HasPrefix(script[0], "'nonce-") || !strings.HasSuffix(script[0], "'") {
|
||||
t.Fatalf("script-src = %q, want exactly one nonce source", script)
|
||||
}
|
||||
nonce := strings.TrimSuffix(strings.TrimPrefix(script[0], "'nonce-"), "'")
|
||||
if nonce == "" {
|
||||
t.Fatal("empty nonce")
|
||||
}
|
||||
for _, d := range []string{"object-src", "base-uri", "frame-ancestors"} {
|
||||
if got := dirs[d]; len(got) != 1 || got[0] != "'none'" {
|
||||
t.Errorf("%s = %q, want 'none'", d, got)
|
||||
}
|
||||
}
|
||||
page := rec.Body.String()
|
||||
tags := regexp.MustCompile(`<script[^>]*>`).FindAllString(page, -1)
|
||||
if len(tags) != 3 {
|
||||
t.Fatalf("script tags = %q, want 3", tags)
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if !strings.Contains(tag, ` nonce="`+nonce+`"`) {
|
||||
t.Errorf("script tag %q lacks nonce %q", tag, nonce)
|
||||
}
|
||||
}
|
||||
for _, sri := range []string{
|
||||
"sha384-Ov4/wv3j2bmct8cDc5X4ngJZohVPzEmc6uDPH8WeljUxO5vtoykvMEfbu9Vh6RaW",
|
||||
"sha384-ZPehFMQommnnuaZ4rpxgkgTT2DKFVp4hZC/7pLit+9Lek9T1YGSo23eHFbvNkXkw",
|
||||
"sha384-My2aDM4r2Mbm3ybHcubKm9O9U8FEjvF/O5nGvE9YK5dzqOTbWEKa79RPJ1krdMaF",
|
||||
} {
|
||||
if !strings.Contains(page, `integrity="`+sri+`"`) {
|
||||
t.Errorf("page lacks integrity %s", sri)
|
||||
}
|
||||
}
|
||||
nonces[nonce] = true
|
||||
}
|
||||
if len(nonces) != 2 {
|
||||
t.Fatal("two requests got the same nonce")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,3 +718,103 @@ func TestTurnstileEmptySecretWarnsAtBoot(t *testing.T) {
|
||||
t.Fatalf("warnings without turnstile routes = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSCredentialedOriginRules(t *testing.T) {
|
||||
// Each block lists a good entry first, so a refusal names index [1].
|
||||
blocks := []struct{ name, prefix, yamlHead, indent string }{
|
||||
{"global", "http.cors", "cors:\n paths: [\"api/*\"]\n", " "},
|
||||
{"profile", "http.cors_profiles.spa", "cors_profiles:\n spa:\n", " "},
|
||||
}
|
||||
load := func(t *testing.T, block int, body string) error {
|
||||
t.Helper()
|
||||
b := blocks[block]
|
||||
cfg := writeHTTPConfig(t, b.yamlHead+body)
|
||||
if b.name == "global" {
|
||||
_, err := LoadCORSConfig(cfg)
|
||||
return err
|
||||
}
|
||||
_, err := LoadCORSProfiles(cfg)
|
||||
return err
|
||||
}
|
||||
entry := func(indent, key, good, value string, credentials bool) string {
|
||||
out := indent + key + ": ['" + good + "', '" + value + "']\n"
|
||||
if credentials {
|
||||
out += indent + "supports_credentials: true\n"
|
||||
}
|
||||
return out
|
||||
}
|
||||
const goodOrigin, goodPattern = "https://www.example.com", `^https://www\.example\.com$`
|
||||
|
||||
accepted := []struct{ key, value string }{
|
||||
{"allowed_origins", "https://www.example.com"},
|
||||
{"allowed_origins", "https://*.example.com"},
|
||||
{"allowed_origins", "https://*.api.example.com:8443"},
|
||||
{"allowed_origins_patterns", `^https://[a-z]+\.example\.org$`},
|
||||
}
|
||||
refused := []struct{ key, value string }{
|
||||
{"allowed_origins", "https://*"},
|
||||
{"allowed_origins", "*://*"},
|
||||
{"allowed_origins", "https://*example.com"},
|
||||
{"allowed_origins", "https://*.com"},
|
||||
{"allowed_origins", "http*://www.example.com"},
|
||||
{"allowed_origins_patterns", `https://.*\.example\.com`},
|
||||
{"allowed_origins_patterns", `^https://.*\.example\.com`},
|
||||
{"allowed_origins_patterns", `^https?://.*$`},
|
||||
{"allowed_origins_patterns", `^.*$`},
|
||||
}
|
||||
for i, b := range blocks {
|
||||
for _, tc := range accepted {
|
||||
good := goodOrigin
|
||||
if tc.key == "allowed_origins_patterns" {
|
||||
good = goodPattern
|
||||
}
|
||||
if err := load(t, i, entry(b.indent, tc.key, good, tc.value, true)); err != nil {
|
||||
t.Errorf("%s %s %q with credentials: %v", b.name, tc.key, tc.value, err)
|
||||
}
|
||||
}
|
||||
for _, tc := range refused {
|
||||
good := goodOrigin
|
||||
if tc.key == "allowed_origins_patterns" {
|
||||
good = goodPattern
|
||||
}
|
||||
want := b.prefix + "." + tc.key + "[1]"
|
||||
err := load(t, i, entry(b.indent, tc.key, good, tc.value, true))
|
||||
if err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("%s %q with credentials: error = %v, want one naming %s", b.name, tc.value, err, want)
|
||||
}
|
||||
if err := load(t, i, entry(b.indent, tc.key, good, tc.value, false)); err != nil {
|
||||
t.Errorf("%s %q without credentials: %v", b.name, tc.value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSAnchoredPatternNoSuffixMatch(t *testing.T) {
|
||||
plugin := routesPluginFunc{id: "acme.spa", routes: func(r pact.Router) {
|
||||
r.Get("/x/me", okHandler, "cors:spa")
|
||||
}}
|
||||
app := transportApp(t, "development", map[string]string{"http.yaml": transportBodyLimits + `cors_profiles:
|
||||
spa:
|
||||
allowed_origins_patterns: ['^https://[a-z]+\.example\.com$']
|
||||
allowed_methods: ["GET"]
|
||||
supports_credentials: true
|
||||
`})
|
||||
h, err := Assemble(app, []party.Plugin{plugin})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := serveCORS(h, corsRequest(http.MethodGet, "/x/me", "https://www.example.com", false))
|
||||
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "https://www.example.com" || rec.Header().Get("Access-Control-Allow-Credentials") != "true" {
|
||||
t.Fatalf("allowed origin: Allow-Origin %q Allow-Credentials %q", got, rec.Header().Get("Access-Control-Allow-Credentials"))
|
||||
}
|
||||
rec = serveCORS(h, corsRequest(http.MethodGet, "/x/me", "https://www.example.com.attacker.net", false))
|
||||
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Fatalf("suffixed origin: Allow-Origin = %q, want none", got)
|
||||
}
|
||||
if rec.Header().Get("Access-Control-Allow-Credentials") != "" {
|
||||
t.Fatal("suffixed origin: Allow-Credentials set")
|
||||
}
|
||||
if !varyHas(rec.Header(), "Origin") {
|
||||
t.Fatalf("suffixed origin: Vary = %q, want Origin", rec.Header().Values("Vary"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user