check / check (push) Failing after 4m24s
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
115 lines
3.5 KiB
Go
115 lines
3.5 KiB
Go
package waf
|
|
|
|
// The heuristic header_limits. It reads no setting: its limits are fixed.
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
)
|
|
|
|
// 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 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" }
|
|
|
|
func (headerLimits) check(
|
|
_ context.Context,
|
|
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 longest <= maxLineBytes && fields <= maxHeaderFields &&
|
|
cookies <= maxCookies {
|
|
return nil
|
|
}
|
|
|
|
return &verdict{
|
|
action: requestlog.ActionRequestHeaderTooLarge,
|
|
figures: map[string]any{
|
|
"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
|
|
// Rule Set runs each of its rules on each cookie, so that thousands of small
|
|
// cookies keep it busy for over a second. It trims only spaces and control
|
|
// characters, of which a header value can hold only tabs, so a part of Unicode
|
|
// spaces is a cookie to it.
|
|
func (rq *request) headerCounts() (int, int) {
|
|
fields, cookies := 0, 0
|
|
|
|
for _, values := range rq.in.Header {
|
|
fields += len(values)
|
|
}
|
|
|
|
for _, value := range rq.in.Header.Values("Cookie") {
|
|
for part := range strings.SplitSeq(value, ";") {
|
|
if strings.Trim(part, " \t") != "" {
|
|
cookies++
|
|
}
|
|
}
|
|
}
|
|
|
|
return fields, cookies
|
|
}
|