Detect CSP weaknesses individually (unsafe-inline, unsafe-eval, missing default-src/script-src, permissive sources on script-src or its default-src fallback) instead of a single catch-all "unsafe" code, and honour CSP3 fetch-directive fallback via EffectiveSources/WildcardSource helpers. Validate Permissions-Policy values: warn when a powerful feature (camera, microphone, geolocation, payment, sensors, …) is granted to all origins. Add a SameSite aggregate state on cookie audits so callers get the global ratio alongside per-cookie diagnostics.
176 lines
6.2 KiB
Go
176 lines
6.2 KiB
Go
// This file is part of the happyDomain (R) project.
|
|
// Copyright (c) 2020-2026 happyDomain
|
|
// Authors: Pierre-Olivier Mercier, et al.
|
|
|
|
package checker
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// runHeaderRule looks up a registered rule by name and evaluates it
|
|
// against an HTTPS probe whose only set header is the one under test.
|
|
// The collector publishes headers as a lowercase-keyed map (see
|
|
// collect.go), so we mirror that here regardless of the casing the
|
|
// caller passed in.
|
|
func runHeaderRule(t *testing.T, ruleName, header, value string) []sdk.CheckState {
|
|
t.Helper()
|
|
p := httpsProbe("a:443")
|
|
if strings.TrimSpace(value) != "" {
|
|
p.Headers[strings.ToLower(header)] = value
|
|
}
|
|
return runRule(t, ruleByName(t, ruleName), &HTTPData{Probes: []HTTPProbe{p}}, nil)
|
|
}
|
|
|
|
func TestReferrerPolicyRule(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
value string
|
|
want sdk.Status
|
|
code string
|
|
}{
|
|
{"missing", "", sdk.StatusInfo, "http.referrer_policy.missing"},
|
|
{"strict-origin-when-cross-origin", "strict-origin-when-cross-origin", sdk.StatusOK, "http.referrer_policy.ok"},
|
|
{"no-referrer", "no-referrer", sdk.StatusOK, "http.referrer_policy.ok"},
|
|
{"unsafe-url", "unsafe-url", sdk.StatusWarn, "http.referrer_policy.invalid"},
|
|
{"no-referrer-when-downgrade", "no-referrer-when-downgrade", sdk.StatusInfo, "http.referrer_policy.invalid"},
|
|
{"unrecognised token", "totally-made-up", sdk.StatusWarn, "http.referrer_policy.invalid"},
|
|
// Per spec the UA picks the last *recognised* token, so the
|
|
// `bogus` is ignored and `same-origin` wins.
|
|
{"list with fallback", "bogus, same-origin", sdk.StatusOK, "http.referrer_policy.ok"},
|
|
// Unknown token after a known one: UA falls back to the last
|
|
// recognised one (`strict-origin`).
|
|
{"list with unknown trailing", "strict-origin, bogus", sdk.StatusOK, "http.referrer_policy.ok"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
states := runHeaderRule(t, "http.referrer_policy", "Referrer-Policy", c.value)
|
|
mustStatus(t, states, c.want)
|
|
if !hasCode(states, c.code) {
|
|
t.Errorf("value=%q: missing code %q in %+v", c.value, c.code, states)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPermissionsPolicyRule(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
value string
|
|
want sdk.Status
|
|
code string
|
|
}{
|
|
{"missing", "", sdk.StatusInfo, "http.permissions_policy.missing"},
|
|
{"restrictive", "camera=(), microphone=()", sdk.StatusOK, "http.permissions_policy.ok"},
|
|
{"self only", "geolocation=(self)", sdk.StatusOK, "http.permissions_policy.ok"},
|
|
{"empty value treated as missing", " ", sdk.StatusInfo, "http.permissions_policy.missing"},
|
|
{"camera wildcard", "camera=*", sdk.StatusWarn, "http.permissions_policy.invalid"},
|
|
{"microphone parenthesised wildcard", "microphone=(*)", sdk.StatusWarn, "http.permissions_policy.invalid"},
|
|
{"non-dangerous wildcard ignored", "fullscreen=(self), accelerometer=*", sdk.StatusWarn, "http.permissions_policy.invalid"},
|
|
{"unknown feature wildcard ignored", "totally-made-up=*", sdk.StatusOK, "http.permissions_policy.ok"},
|
|
{"malformed entry", "camera", sdk.StatusWarn, "http.permissions_policy.invalid"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
states := runHeaderRule(t, "http.permissions_policy", "Permissions-Policy", c.value)
|
|
mustStatus(t, states, c.want)
|
|
if !hasCode(states, c.code) {
|
|
t.Errorf("value=%q: missing code %q in %+v", c.value, c.code, states)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCOOPRule(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
value string
|
|
want sdk.Status
|
|
code string
|
|
}{
|
|
{"missing", "", sdk.StatusInfo, "http.coop.missing"},
|
|
{"same-origin", "same-origin", sdk.StatusOK, "http.coop.ok"},
|
|
{"same-origin-allow-popups", "same-origin-allow-popups", sdk.StatusOK, "http.coop.ok"},
|
|
{"unsafe-none", "unsafe-none", sdk.StatusWarn, "http.coop.invalid"},
|
|
{"unrecognised", "bogus", sdk.StatusWarn, "http.coop.invalid"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
states := runHeaderRule(t, "http.coop", "Cross-Origin-Opener-Policy", c.value)
|
|
mustStatus(t, states, c.want)
|
|
if !hasCode(states, c.code) {
|
|
t.Errorf("value=%q: missing code %q in %+v", c.value, c.code, states)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCOEPRule(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
value string
|
|
want sdk.Status
|
|
code string
|
|
}{
|
|
{"missing", "", sdk.StatusInfo, "http.coep.missing"},
|
|
{"require-corp", "require-corp", sdk.StatusOK, "http.coep.ok"},
|
|
{"credentialless", "credentialless", sdk.StatusOK, "http.coep.ok"},
|
|
{"unsafe-none", "unsafe-none", sdk.StatusWarn, "http.coep.invalid"},
|
|
{"unrecognised", "bogus", sdk.StatusWarn, "http.coep.invalid"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
states := runHeaderRule(t, "http.coep", "Cross-Origin-Embedder-Policy", c.value)
|
|
mustStatus(t, states, c.want)
|
|
if !hasCode(states, c.code) {
|
|
t.Errorf("value=%q: missing code %q in %+v", c.value, c.code, states)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCORPRule(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
value string
|
|
want sdk.Status
|
|
code string
|
|
}{
|
|
{"missing", "", sdk.StatusInfo, "http.corp.missing"},
|
|
{"same-origin", "same-origin", sdk.StatusOK, "http.corp.ok"},
|
|
{"same-site", "same-site", sdk.StatusOK, "http.corp.ok"},
|
|
{"cross-origin", "cross-origin", sdk.StatusOK, "http.corp.ok"},
|
|
{"unrecognised", "bogus", sdk.StatusWarn, "http.corp.invalid"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
states := runHeaderRule(t, "http.corp", "Cross-Origin-Resource-Policy", c.value)
|
|
mustStatus(t, states, c.want)
|
|
if !hasCode(states, c.code) {
|
|
t.Errorf("value=%q: missing code %q in %+v", c.value, c.code, states)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestModernHeaders_NoHTTPS(t *testing.T) {
|
|
// Each modern header rule must emit Unknown when there are no
|
|
// successful HTTPS probes — the no_https path comes from EvalPerHTTPS.
|
|
rules := []string{
|
|
"http.referrer_policy",
|
|
"http.permissions_policy",
|
|
"http.coop",
|
|
"http.coep",
|
|
"http.corp",
|
|
}
|
|
data := &HTTPData{Probes: []HTTPProbe{httpProbe("a:80")}}
|
|
for _, name := range rules {
|
|
t.Run(name, func(t *testing.T) {
|
|
states := runRule(t, ruleByName(t, name), data, nil)
|
|
mustStatus(t, states, sdk.StatusUnknown)
|
|
})
|
|
}
|
|
}
|