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 }