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
@@ -10,16 +10,22 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
)
|
||||
|
||||
// The header limits: the most cookies and header fields a request may have.
|
||||
// Real clients never reach them.
|
||||
// The header limits: the most cookies and header fields a request may have,
|
||||
// and the longest its request line and each header line may be, in bytes, not
|
||||
// counting the line ending. Real clients rarely reach them. maxLineBytes is
|
||||
// what nginx allows by default.
|
||||
const (
|
||||
maxCookies = 50
|
||||
maxHeaderFields = 100
|
||||
maxLineBytes = 8 << 10
|
||||
)
|
||||
|
||||
// headerLimits refuses a request with more than maxHeaderFields header fields
|
||||
// or more than maxCookies cookies, with 431. Its figures are the request's
|
||||
// header fields and cookies, and the limit of each.
|
||||
// headerLimits refuses a request with a request line longer than maxLineBytes,
|
||||
// with 414. It refuses a request with a header line longer than maxLineBytes,
|
||||
// more than maxHeaderFields header fields or more than maxCookies cookies, with
|
||||
// 431. Its figures are the request line's length and its limit for 414. For
|
||||
// 431 they are the longest header line, the header fields and the cookies,
|
||||
// each with its limit.
|
||||
type headerLimits struct{}
|
||||
|
||||
func (headerLimits) name() string { return "header_limits" }
|
||||
@@ -29,20 +35,59 @@ func (headerLimits) check(
|
||||
rq *request,
|
||||
_ time.Time,
|
||||
) *verdict {
|
||||
requestLine := rq.requestLineBytes()
|
||||
if requestLine > maxLineBytes {
|
||||
return &verdict{
|
||||
action: requestlog.ActionRequestLineTooLong,
|
||||
figures: map[string]any{
|
||||
"request_line_bytes": requestLine,
|
||||
"request_line_bytes_limit": maxLineBytes,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
longest := rq.longestHeaderLineBytes()
|
||||
fields, cookies := rq.headerCounts()
|
||||
if fields <= maxHeaderFields && cookies <= maxCookies {
|
||||
|
||||
if longest <= maxLineBytes && fields <= maxHeaderFields &&
|
||||
cookies <= maxCookies {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &verdict{
|
||||
action: requestlog.ActionRequestHeaderTooLarge,
|
||||
figures: map[string]any{
|
||||
"fields": fields, "fields_limit": maxHeaderFields,
|
||||
"longest_header_line_bytes": longest,
|
||||
"header_line_bytes_limit": maxLineBytes,
|
||||
"fields": fields, "fields_limit": maxHeaderFields,
|
||||
"cookies": cookies, "cookies_limit": maxCookies,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// requestLineBytes returns the length of the request's request line, such as
|
||||
// "GET /search?q=x HTTP/1.1", not counting its line ending. The target is the
|
||||
// one the visitor sent, which rproxy's question carries unchanged.
|
||||
func (rq *request) requestLineBytes() int {
|
||||
return len(rq.in.Method) + len(" ") + len(rq.in.RequestURI) +
|
||||
len(" ") + len(rq.in.Proto)
|
||||
}
|
||||
|
||||
// longestHeaderLineBytes returns the length of the request's longest header
|
||||
// line, such as "Cookie: a=1", not counting its line ending. Each value of a
|
||||
// header sent twice is a line of its own, and Host is a line too.
|
||||
func (rq *request) longestHeaderLineBytes() int {
|
||||
longest := len("Host: ") + len(rq.in.Host)
|
||||
|
||||
for name, values := range rq.in.Header {
|
||||
for _, value := range values {
|
||||
longest = max(longest, len(name)+len(": ")+len(value))
|
||||
}
|
||||
}
|
||||
|
||||
return longest
|
||||
}
|
||||
|
||||
// headerCounts returns the request's header fields, each value of a header sent
|
||||
// twice being one, and its cookies, each part of a Cookie header between
|
||||
// semicolons being one unless it is empty or only spaces and tabs. The Core
|
||||
|
||||
Reference in new issue
Block a user