diff --git a/README.md b/README.md index de9da0d..aea45f8 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ relies on TLS for transport. | `http.x_content_type_options` | Verifies that responses set X-Content-Type-Options: nosniff. | WARNING | | `http.x_xss_protection` | Reports the legacy X-XSS-Protection header; warns on filtering mode (can introduce XSS), absent/`0` are fine, CSP is the real defense. | WARNING | | `http.expect_ct` | Flags the deprecated Expect-CT header (Certificate Transparency is now enforced by mainstream clients; Mozilla recommends removing it). | WARNING | +| `http.hpkp` | Flags the deprecated Public-Key-Pins (HPKP) header, which is unsupported by modern browsers; rely on Certificate Transparency and CAA records instead. | WARNING | +| `http.hpkp_report_only` | Flags the deprecated Public-Key-Pins-Report-Only (HPKP) header, which is unsupported by modern browsers; rely on Certificate Transparency and CAA records instead. | WARNING | | `http.server_header` | Reports whether the Server header discloses the origin server software/version; a non-informative value is accepted. | INFO | | `http.x_powered_by` | Reports the X-Powered-By header, which discloses the web server technology stack and should be removed. | INFO | | `http.x_aspnet_version` | Reports the X-AspNet-Version header, which discloses the ASP.NET framework version and should be removed. | INFO | diff --git a/checker/rules_security_headers.go b/checker/rules_security_headers.go index 0a23e10..1952654 100644 --- a/checker/rules_security_headers.go +++ b/checker/rules_security_headers.go @@ -112,6 +112,38 @@ func init() { RegisterRule(disclosureHeaderRule("http.x_powered_by", "X-Powered-By", "the technologies used by the web server")) RegisterRule(disclosureHeaderRule("http.x_aspnet_version", "X-AspNet-Version", "the ASP.NET framework version")) RegisterRule(disclosureHeaderRule("http.x_aspnetmvc_version", "X-AspNetMvc-Version", "the ASP.NET MVC version")) + + RegisterRule(deprecatedHPKPRule("http.hpkp", "Public-Key-Pins")) + RegisterRule(deprecatedHPKPRule("http.hpkp_report_only", "Public-Key-Pins-Report-Only")) +} + +// deprecatedHPKPRule builds a rule for the HTTP Public-Key-Pins (HPKP) +// headers. Key pinning was removed from Chromium in 2018 and is +// unsupported by all modern browsers; its operational brittleness made it +// a frequent cause of self-inflicted outages. Certificate Transparency +// and CAA records provide superior compromise detection, so any presence +// is reported as Warn ".deprecated" and absence is OK ".absent". +func deprecatedHPKPRule(code, header string) sdk.CheckRule { + return HeaderRule(HeaderRuleSpec{ + Code: code, + Description: "Reports the presence of the deprecated " + header + " (HPKP) header, which is unsupported by modern browsers and should be removed.", + Header: header, + Inspect: func(_ string, _ HTTPProbe, _ sdk.CheckerOptions) []HeaderResult { + return []HeaderResult{{ + Status: sdk.StatusWarn, + Suffix: "deprecated", + Message: header + " (HPKP) is deprecated. Key pinning was removed from Chromium in 2018 and is unsupported by modern browsers; rely on Certificate Transparency and CAA DNS records instead.", + Meta: map[string]any{"fix": "Remove the `" + header + "` header from your responses."}, + }} + }, + OnMissing: func(_ HTTPProbe, _ sdk.CheckerOptions) []HeaderResult { + return []HeaderResult{{ + Status: sdk.StatusOK, + Suffix: "absent", + Message: header + " is not set, which is correct (HPKP is deprecated).", + }} + }, + }) } // Information-disclosure headers -------------------------------------- diff --git a/checker/rules_security_headers_test.go b/checker/rules_security_headers_test.go index 6308850..9875dca 100644 --- a/checker/rules_security_headers_test.go +++ b/checker/rules_security_headers_test.go @@ -353,6 +353,34 @@ func TestDisclosureHeaderRules(t *testing.T) { } } +func TestHPKPRules(t *testing.T) { + cases := []struct { + rule string + header string + }{ + {"http.hpkp", "public-key-pins"}, + {"http.hpkp_report_only", "public-key-pins-report-only"}, + } + for _, c := range cases { + t.Run(c.rule, func(t *testing.T) { + // Absent → OK, since HPKP is deprecated. + states := runRule(t, ruleByName(t, c.rule), &HTTPData{Probes: []HTTPProbe{httpsProbe("a:443")}}, nil) + mustStatus(t, states, sdk.StatusOK) + if !hasCode(states, c.rule+".absent") { + t.Errorf("%s: missing absent code: %+v", c.rule, states) + } + // Present → Warn deprecated. + p := httpsProbe("a:443") + p.Headers[c.header] = `pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="; max-age=5184000` + states = runRule(t, ruleByName(t, c.rule), &HTTPData{Probes: []HTTPProbe{p}}, nil) + mustStatus(t, states, sdk.StatusWarn) + if !hasCode(states, c.rule+".deprecated") { + t.Errorf("%s: missing deprecated code: %+v", c.rule, states) + } + }) + } +} + func TestSecurityHeaders_NoHTTPS(t *testing.T) { // Each header rule must emit Unknown when there are no successful HTTPS probes. rules := []sdk.CheckRule{ @@ -366,6 +394,8 @@ func TestSecurityHeaders_NoHTTPS(t *testing.T) { ruleByName(t, "http.x_powered_by"), ruleByName(t, "http.x_aspnet_version"), ruleByName(t, "http.x_aspnetmvc_version"), + ruleByName(t, "http.hpkp"), + ruleByName(t, "http.hpkp_report_only"), } data := &HTTPData{Probes: []HTTPProbe{httpProbe("a:80")}} for _, r := range rules {