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.
122 lines
4.1 KiB
Go
122 lines
4.1 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"
|
|
)
|
|
|
|
func TestCookieFlagsRule_NoHTTPS(t *testing.T) {
|
|
data := &HTTPData{Probes: []HTTPProbe{httpProbe("a:80")}}
|
|
states := runRule(t, &cookieFlagsRule{}, data, nil)
|
|
mustStatus(t, states, sdk.StatusUnknown)
|
|
}
|
|
|
|
func TestCookieFlagsRule_NoCookies(t *testing.T) {
|
|
data := &HTTPData{Probes: []HTTPProbe{httpsProbe("a:443")}}
|
|
states := runRule(t, &cookieFlagsRule{}, data, nil)
|
|
mustStatus(t, states, sdk.StatusOK)
|
|
if !hasCode(states, "http.cookie_flags.none") {
|
|
t.Errorf("missing 'none' code: %+v", states)
|
|
}
|
|
}
|
|
|
|
func TestCookieFlagsRule_AllOK(t *testing.T) {
|
|
p := httpsProbe("a:443")
|
|
p.Cookies = []CookieInfo{
|
|
{Name: "sid", Secure: true, HttpOnly: true, SameSite: "Strict"},
|
|
{Name: "tok", Secure: true, HttpOnly: true, SameSite: "Lax"},
|
|
}
|
|
states := runRule(t, &cookieFlagsRule{}, &HTTPData{Probes: []HTTPProbe{p}}, nil)
|
|
mustStatus(t, states, sdk.StatusOK)
|
|
if !hasCode(states, "http.cookie_flags.ok") {
|
|
t.Errorf("missing ok code: %+v", states)
|
|
}
|
|
}
|
|
|
|
func TestCookieFlagsRule_Issues(t *testing.T) {
|
|
p := httpsProbe("a:443")
|
|
p.Cookies = []CookieInfo{
|
|
{Name: "no-secure", Secure: false, HttpOnly: true, SameSite: "Lax"},
|
|
{Name: "no-httponly", Secure: true, HttpOnly: false, SameSite: "Lax"},
|
|
{Name: "no-samesite", Secure: true, HttpOnly: true, SameSite: ""},
|
|
{Name: "none-without-secure", Secure: false, HttpOnly: true, SameSite: "None"},
|
|
}
|
|
states := runRule(t, &cookieFlagsRule{}, &HTTPData{Probes: []HTTPProbe{p}}, nil)
|
|
// Per-cookie diagnostics + a single SameSite aggregate (1 cookie out
|
|
// of 4 is missing SameSite).
|
|
if len(states) != len(p.Cookies)+1 {
|
|
t.Fatalf("got %d states, want %d", len(states), len(p.Cookies)+1)
|
|
}
|
|
mustStatus(t, states, sdk.StatusWarn)
|
|
|
|
if !hasCode(states, "http.cookie_flags.samesite_missing") {
|
|
t.Errorf("missing samesite_missing aggregate: %+v", states)
|
|
}
|
|
for _, st := range states {
|
|
if st.Code == "http.cookie_flags.samesite_missing" {
|
|
if !strings.Contains(st.Message, "1 of 4") {
|
|
t.Errorf("aggregate message %q should mention 1 of 4", st.Message)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check each per-cookie diagnostic mentions the cookie name and a
|
|
// relevant phrase.
|
|
wantSubstr := map[string]string{
|
|
"no-secure": "missing Secure",
|
|
"no-httponly": "missing HttpOnly",
|
|
"no-samesite": "missing SameSite",
|
|
"none-without-secure": "SameSite=None requires Secure",
|
|
}
|
|
for _, st := range states {
|
|
if st.Code != "http.cookie_flags.weak" {
|
|
continue
|
|
}
|
|
matched := false
|
|
for name, phrase := range wantSubstr {
|
|
if strings.Contains(st.Message, name) && strings.Contains(st.Message, phrase) {
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
if !matched {
|
|
t.Errorf("unexpected state message: %q", st.Message)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCookieFlagsRule_SameSiteAggregateOnly(t *testing.T) {
|
|
// Two cookies, both otherwise compliant but missing SameSite. We
|
|
// expect 2 per-cookie warnings + 1 aggregate.
|
|
p := httpsProbe("a:443")
|
|
p.Cookies = []CookieInfo{
|
|
{Name: "a", Secure: true, HttpOnly: true, SameSite: ""},
|
|
{Name: "b", Secure: true, HttpOnly: true, SameSite: ""},
|
|
}
|
|
states := runRule(t, &cookieFlagsRule{}, &HTTPData{Probes: []HTTPProbe{p}}, nil)
|
|
mustStatus(t, states, sdk.StatusWarn)
|
|
if !hasCode(states, "http.cookie_flags.samesite_missing") {
|
|
t.Fatalf("missing aggregate state: %+v", states)
|
|
}
|
|
for _, st := range states {
|
|
if st.Code == "http.cookie_flags.samesite_missing" && !strings.Contains(st.Message, "2 of 2") {
|
|
t.Errorf("aggregate should report 2 of 2, got %q", st.Message)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCookieFlagsRule_SameSiteNoneCaseInsensitive(t *testing.T) {
|
|
p := httpsProbe("a:443")
|
|
p.Cookies = []CookieInfo{{Name: "x", Secure: false, HttpOnly: true, SameSite: "none"}}
|
|
states := runRule(t, &cookieFlagsRule{}, &HTTPData{Probes: []HTTPProbe{p}}, nil)
|
|
mustStatus(t, states, sdk.StatusWarn)
|
|
if !strings.Contains(states[0].Message, "SameSite=None requires Secure") {
|
|
t.Errorf("expected SameSite=None warning regardless of casing, got %q", states[0].Message)
|
|
}
|
|
}
|