waf: refuse request lines and header lines over 8 KiB first (closes #339)
check / check (push) Failing after 1m47s
check / check (push) Failing after 1m47s
The header limits now also refuse a request line over 8 KiB with 414 (action request_line_too_long) and a header line over 8 KiB, Host included, with 431. Like the other header limits they are fixed, run first and make no ban. One long query string or cookie no longer keeps the Core Rule Set busy for a third of a second. Model: opus-5-5
This commit is contained in:
1 parent
3550956cbb
commit
b0ac6fd9ce
10 files changed
+241
-39
No files matched your search
@@ -39,6 +39,7 @@ const (
|
||||
// The header limits SPEC.md gives.
|
||||
cookieLimit = 50
|
||||
headerFieldLimit = 100
|
||||
lineLimit = 8192
|
||||
// cookieHeader is the header the cookie limit counts.
|
||||
cookieHeader = "Cookie"
|
||||
// tooLarge is how waf refuses a request over the header limits.
|
||||
@@ -104,11 +105,139 @@ func TestHeaderLimitRefusalNamesItsHeuristicCountsAndLimits(t *testing.T) {
|
||||
fillHeaders(req, headerFieldLimit+1)
|
||||
do(t, req)
|
||||
|
||||
// The longest header line is X-Request-ID's, with its 26-character id.
|
||||
wantHeuristic(
|
||||
t,
|
||||
s.out.requestLine(t),
|
||||
"header_limits",
|
||||
`{"fields": 101, "fields_limit": 100, "cookies": 0, "cookies_limit": 50}`,
|
||||
`{"longest_header_line_bytes": 40, "header_line_bytes_limit": 8192,
|
||||
"fields": 101, "fields_limit": 100, "cookies": 0, "cookies_limit": 50}`,
|
||||
)
|
||||
}
|
||||
|
||||
// longQuery returns a path whose request line, sent by Go's client as
|
||||
// "GET <path> HTTP/1.1", is lineBytes long.
|
||||
func longQuery(lineBytes int) string {
|
||||
const around = len("GET /?q= HTTP/1.1")
|
||||
|
||||
return "/?q=" + strings.Repeat("a", lineBytes-around)
|
||||
}
|
||||
|
||||
func TestARequestLineOverTheLimitIsRefusedWith414BeforeAnyOtherCheck(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
lineBytes int
|
||||
// cookies is how many cookies the request carries.
|
||||
cookies int
|
||||
status int
|
||||
action string
|
||||
}{
|
||||
{
|
||||
"at the limit", lineLimit, 0, http.StatusOK,
|
||||
requestlog.ActionForward,
|
||||
},
|
||||
{
|
||||
"over the limit", lineLimit + 1, 0, http.StatusRequestURITooLong,
|
||||
requestlog.ActionRequestLineTooLong,
|
||||
},
|
||||
{
|
||||
"over the limit and the cookie limit", lineLimit + 1,
|
||||
cookieLimit + 1, http.StatusRequestURITooLong,
|
||||
requestlog.ActionRequestLineTooLong,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := startWAF(t, "", map[string]string{coreRuleSetMode: block})
|
||||
req := s.question(siteA, client, longQuery(tc.lineBytes))
|
||||
|
||||
if tc.cookies > 0 {
|
||||
req.Header.Set(cookieHeader, cookies(tc.cookies))
|
||||
}
|
||||
|
||||
got := do(t, req)
|
||||
line := s.out.requestLine(t)
|
||||
|
||||
if tc.status == http.StatusOK {
|
||||
wantDisposition(t, got, tc.status, waf.DispositionAllow)
|
||||
} else {
|
||||
wantDisposition(t, got, tc.status, waf.DispositionDeny)
|
||||
}
|
||||
|
||||
wantLine(t, line, tc.status, tc.action)
|
||||
|
||||
refused := tc.status != http.StatusOK
|
||||
if refused != (line.DurationCoreRuleSetMS == nil) {
|
||||
t.Errorf(
|
||||
"Core Rule Set time %v for a request refused: %t",
|
||||
line.DurationCoreRuleSetMS,
|
||||
refused,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAHeaderLineOverTheLimitIsRefusedWith431(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
make func(*http.Request)
|
||||
// refused is true for a request over the limit, and false for one at it.
|
||||
refused bool
|
||||
}{
|
||||
{"a header line at the limit", func(req *http.Request) {
|
||||
req.Header.Set("X-Long", strings.Repeat("a", lineLimit-len("X-Long: ")))
|
||||
}, false},
|
||||
{"a header line over the limit", func(req *http.Request) {
|
||||
req.Header.Set("X-Long", strings.Repeat("a", lineLimit+1-len("X-Long: ")))
|
||||
}, true},
|
||||
{"one cookie over the limit", func(req *http.Request) {
|
||||
req.Header.Set(cookieHeader, "a="+strings.Repeat("a", lineLimit))
|
||||
}, true},
|
||||
{"a Host line over the limit", func(req *http.Request) {
|
||||
req.Host = strings.Repeat("a", lineLimit+1-len("Host: "))
|
||||
}, true},
|
||||
{"a header sent twice, each line within the limit", func(req *http.Request) {
|
||||
req.Header.Add("X-Long", strings.Repeat("a", 8000))
|
||||
req.Header.Add("X-Long", strings.Repeat("a", 8000))
|
||||
}, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := startWAF(t, "", nil)
|
||||
req := s.question(siteA, client, "/")
|
||||
tc.make(req)
|
||||
|
||||
got := do(t, req)
|
||||
|
||||
if tc.refused {
|
||||
wantDisposition(t, got, tooLarge, waf.DispositionDeny)
|
||||
} else {
|
||||
wantDisposition(t, got, http.StatusOK, waf.DispositionAllow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestLineRefusalNamesItsHeuristicLengthAndLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := startWAF(t, "", nil)
|
||||
do(t, s.question(siteA, client, longQuery(lineLimit+1)))
|
||||
|
||||
wantHeuristic(
|
||||
t,
|
||||
s.out.requestLine(t),
|
||||
"header_limits",
|
||||
`{"request_line_bytes": 8193, "request_line_bytes_limit": 8192}`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user