checker: flag information-disclosure response headers

Report Server, X-Powered-By, X-AspNet-Version and X-AspNetMvc-Version
headers, whose presence leaks the server/framework stack. Server only
warns on informative values (version/product detail), accepting a
non-informative value per OWASP.
This commit is contained in:
nemunaire 2026-06-18 10:59:54 +09:00
commit a0fb42223b
3 changed files with 145 additions and 0 deletions

View file

@ -296,6 +296,63 @@ func TestExpectCTRule(t *testing.T) {
}
}
func TestServerHeaderRule(t *testing.T) {
cases := []struct {
val string
want sdk.Status
code string
}{
{"", sdk.StatusOK, "http.server_header.absent"},
{"nginx/1.18.0", sdk.StatusInfo, "http.server_header.disclosed"},
{"Apache/2.4.41 (Ubuntu)", sdk.StatusInfo, "http.server_header.disclosed"},
// Bare product token with a separator is still informative.
{"nginx/", sdk.StatusInfo, "http.server_header.disclosed"},
// Non-informative value is acceptable per OWASP.
{"webserver", sdk.StatusOK, "http.server_header.ok"},
{"nginx", sdk.StatusOK, "http.server_header.ok"},
}
for _, c := range cases {
p := httpsProbe("a:443")
if c.val != "" {
p.Headers["server"] = c.val
}
states := runRule(t, ruleByName(t, "http.server_header"), &HTTPData{Probes: []HTTPProbe{p}}, nil)
mustStatus(t, states, c.want)
if !hasCode(states, c.code) {
t.Errorf("val=%q: want code %q, got %+v", c.val, c.code, states)
}
}
}
func TestDisclosureHeaderRules(t *testing.T) {
cases := []struct {
rule string
header string
}{
{"http.x_powered_by", "x-powered-by"},
{"http.x_aspnet_version", "x-aspnet-version"},
{"http.x_aspnetmvc_version", "x-aspnetmvc-version"},
}
for _, c := range cases {
t.Run(c.rule, func(t *testing.T) {
// Absent → OK.
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 → Info disclosure.
p := httpsProbe("a:443")
p.Headers[c.header] = "4.0.30319"
states = runRule(t, ruleByName(t, c.rule), &HTTPData{Probes: []HTTPProbe{p}}, nil)
mustStatus(t, states, sdk.StatusInfo)
if !hasCode(states, c.rule+".disclosed") {
t.Errorf("%s: missing disclosed 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{
@ -305,6 +362,10 @@ func TestSecurityHeaders_NoHTTPS(t *testing.T) {
ruleByName(t, "http.x_content_type_options"),
ruleByName(t, "http.x_xss_protection"),
ruleByName(t, "http.expect_ct"),
ruleByName(t, "http.server_header"),
ruleByName(t, "http.x_powered_by"),
ruleByName(t, "http.x_aspnet_version"),
ruleByName(t, "http.x_aspnetmvc_version"),
}
data := &HTTPData{Probes: []HTTPProbe{httpProbe("a:80")}}
for _, r := range rules {