// This file is part of the happyDomain (R) project. // Copyright (c) 2020-2026 happyDomain // Authors: Pierre-Olivier Mercier, et al. package checker import ( "strings" sdk "git.happydns.org/checker-sdk-go/checker" ) // This file wires modern privacy/isolation headers entirely through the // HeaderRule DSL. Each rule is a single declarative spec — no per-rule // type, no Evaluate plumbing, no test scaffolding beyond the value // validator. // // Coverage: // - Referrer-Policy (W3C Referrer Policy) // - Permissions-Policy (W3C Permissions Policy, replaces Feature-Policy) // - Cross-Origin-Opener-Policy (HTML spec, COOP) // - Cross-Origin-Embedder-Policy (HTML spec, COEP) // - Cross-Origin-Resource-Policy (Fetch spec, CORP) // // These are all "presence + value sanity" checks. Anything richer (e.g. // directive-by-directive Permissions-Policy parsing) belongs in its own // hand-rolled rule. func init() { RegisterRule(HeaderRule(HeaderRuleSpec{ Code: "http.referrer_policy", Description: "Verifies that responses set a Referrer-Policy header with a privacy-preserving value.", Header: "Referrer-Policy", Required: false, FixHint: "Send `Referrer-Policy: strict-origin-when-cross-origin` (the modern browser default) or stricter.", Validate: validateReferrerPolicy, })) RegisterRule(HeaderRule(HeaderRuleSpec{ Code: "http.permissions_policy", Description: "Reports the presence of a Permissions-Policy header (W3C Permissions Policy, replaces Feature-Policy).", Header: "Permissions-Policy", Required: false, FixHint: "Define a Permissions-Policy that disables APIs the site does not use, e.g. `Permissions-Policy: camera=(), microphone=(), geolocation=()`.", })) RegisterRule(HeaderRule(HeaderRuleSpec{ Code: "http.coop", Description: "Verifies the Cross-Origin-Opener-Policy (COOP) header for cross-origin process isolation.", Header: "Cross-Origin-Opener-Policy", Required: false, FixHint: "Send `Cross-Origin-Opener-Policy: same-origin` to isolate this document from cross-origin windows.", Validate: validateCOOP, })) RegisterRule(HeaderRule(HeaderRuleSpec{ Code: "http.coep", Description: "Verifies the Cross-Origin-Embedder-Policy (COEP) header. Required (with COOP) to enable cross-origin isolation and APIs such as SharedArrayBuffer.", Header: "Cross-Origin-Embedder-Policy", Required: false, FixHint: "Send `Cross-Origin-Embedder-Policy: require-corp` (or `credentialless`) once embedded resources opt in via CORP/CORS.", Validate: validateCOEP, })) RegisterRule(HeaderRule(HeaderRuleSpec{ Code: "http.corp", Description: "Verifies the Cross-Origin-Resource-Policy (CORP) header, which lets a server forbid cross-origin/cross-site embedding of its responses.", Header: "Cross-Origin-Resource-Policy", Required: false, FixHint: "Send `Cross-Origin-Resource-Policy: same-origin` (or `same-site`) on responses that should not be embedded cross-origin.", Validate: validateCORP, })) } // validateReferrerPolicy accepts any token (or comma-separated list of // tokens) defined by the W3C Referrer Policy spec, but downgrades the // status when the only effective value is the historically lax // `unsafe-url` or `no-referrer-when-downgrade`. Per the spec, browsers // pick the last *recognised* token of a comma list, so we evaluate that // one. func validateReferrerPolicy(v string) (sdk.Status, string) { tokens := splitCSV(v) if len(tokens) == 0 { return sdk.StatusWarn, "Referrer-Policy is empty." } // Per spec, the user-agent picks the last token it recognises. var effective string for _, t := range tokens { if isReferrerPolicyToken(t) { effective = t } } if effective == "" { return sdk.StatusWarn, "Referrer-Policy has no recognised token: " + v } switch effective { case "unsafe-url": return sdk.StatusWarn, "Referrer-Policy: unsafe-url leaks the full URL (including query) cross-origin; prefer strict-origin-when-cross-origin." case "no-referrer-when-downgrade": return sdk.StatusInfo, "Referrer-Policy: no-referrer-when-downgrade is the legacy default; prefer strict-origin-when-cross-origin." } return sdk.StatusOK, "Referrer-Policy is set to " + effective + "." } func isReferrerPolicyToken(t string) bool { switch t { case "no-referrer", "no-referrer-when-downgrade", "origin", "origin-when-cross-origin", "same-origin", "strict-origin", "strict-origin-when-cross-origin", "unsafe-url", "": return t != "" } return false } func validateCOOP(v string) (sdk.Status, string) { switch strings.ToLower(directiveToken(v)) { case "same-origin", "same-origin-allow-popups", "noopener-allow-popups": return sdk.StatusOK, "Cross-Origin-Opener-Policy is set to " + v + "." case "unsafe-none": return sdk.StatusWarn, "Cross-Origin-Opener-Policy: unsafe-none disables the protection (this is the browser default; the header is redundant)." } return sdk.StatusWarn, "Cross-Origin-Opener-Policy has an unrecognised value: " + v } func validateCOEP(v string) (sdk.Status, string) { switch strings.ToLower(directiveToken(v)) { case "require-corp", "credentialless": return sdk.StatusOK, "Cross-Origin-Embedder-Policy is set to " + v + "." case "unsafe-none": return sdk.StatusWarn, "Cross-Origin-Embedder-Policy: unsafe-none disables the protection (this is the browser default; the header is redundant)." } return sdk.StatusWarn, "Cross-Origin-Embedder-Policy has an unrecognised value: " + v } func validateCORP(v string) (sdk.Status, string) { switch strings.ToLower(directiveToken(v)) { case "same-origin", "same-site", "cross-origin": return sdk.StatusOK, "Cross-Origin-Resource-Policy is set to " + v + "." } return sdk.StatusWarn, "Cross-Origin-Resource-Policy has an unrecognised value: " + v } // splitCSV splits on commas, trims whitespace, lowercases, and drops // empty fragments. Used for header values that are comma-separated lists // of tokens (Referrer-Policy, Accept-Encoding, …). func splitCSV(v string) []string { parts := strings.Split(v, ",") out := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(strings.ToLower(p)) if p != "" { out = append(out, p) } } return out } // directiveToken extracts the first whitespace-delimited token of a // header value, stripping any trailing parameters (e.g. `same-origin // "..."` -> `same-origin`). Suitable for single-token directive headers // like COOP/COEP/CORP. func directiveToken(v string) string { v = strings.TrimSpace(v) if i := strings.IndexAny(v, " \t;,"); i >= 0 { return v[:i] } return v }