waf: refuse request lines and header lines over 8 KiB first (closes #339)
check / check (push) Failing after 3m35s
check / check (push) Failing after 3m35s
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
50c50bfcae
commit
1f2699c591
10 files changed
+221
-40
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.
|
||||
@@ -107,11 +108,119 @@ 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
|
||||
status int
|
||||
action string
|
||||
}{
|
||||
{"at the limit", lineLimit, http.StatusOK, requestlog.ActionForward},
|
||||
{
|
||||
"over the limit", lineLimit + 1, http.StatusRequestURITooLong,
|
||||
requestlog.ActionRequestLineTooLong,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := startWAF(t, "", map[string]string{coreRuleSetMode: block})
|
||||
got := do(t, s.question(siteA, client, longQuery(tc.lineBytes)))
|
||||
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},
|
||||
} {
|
||||
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