Leave SWWAF_RATE_LIMIT_EXEMPT_PATHS out of the request rate limits (closes #77)
check / check (push) Successful in 4m48s

A request whose path, as the client sent it and before the query
string, starts with one of the comma-separated prefixes in
SWWAF_RATE_LIMIT_EXEMPT_PATHS is neither counted nor refused by the
request rate limits; the static lists, bans and the country lists still
apply. The setting is empty by default, and a prefix that does not start
with / stops the start. README.md documents it.

Judgement call: plain prefix on the path as sent, as the issue rules, so
/assets/../login matches /assets/; traefik removes such dot segments by
default, but a request that reaches smallwebwaf uncleaned is matched as
sent.

Model: opus-5-5
This commit is contained in:
2026-10-06 10:06:38 +00:00
parent 234c5eac60
commit 611ae5c008
7 changed files with 131 additions and 16 deletions
+14 -2
View File
@@ -8,6 +8,8 @@ import (
"net/http/httputil"
"net/netip"
"os"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
@@ -115,13 +117,16 @@ func (h *handler) newRequest(w http.ResponseWriter, r *http.Request) *request {
// client either refuses is not looked up, and then the country lists; a
// request any of them refuses is not counted for the rate limits. Then
// come the rate limits, unless the client is in
// SWWAF_RATE_LIMIT_EXEMPT_NETS, so that every other request is counted,
// SWWAF_RATE_LIMIT_EXEMPT_NETS or the request's path, as the client sent
// it and the log line shows it, starts with one of
// SWWAF_RATE_LIMIT_EXEMPT_PATHS, so that every other request is counted,
// one refused for its size too. Every refusal but the size limit's is
// answered with SWWAF_BAN_RESPONSE. ctx is the request's own context.
func (rq *request) check(ctx context.Context) *refusal {
cfg := rq.h.config
allowed := isInside(rq.client, cfg.AllowNets)
exempt := isInside(rq.client, cfg.RateLimitExemptNets)
exempt := isInside(rq.client, cfg.RateLimitExemptNets) ||
startsWithAny(rq.in.URL.EscapedPath(), cfg.RateLimitExemptPaths)
now := rq.h.now()
if !allowed && isInside(rq.client, cfg.DenyNets) {
@@ -152,6 +157,13 @@ func (rq *request) check(ctx context.Context) *refusal {
return nil
}
// startsWithAny reports whether path starts with one of prefixes.
func startsWithAny(path string, prefixes []string) bool {
return slices.ContainsFunc(prefixes, func(prefix string) bool {
return strings.HasPrefix(path, prefix)
})
}
// forward passes the request to the app and the app's answer back. ctx
// is the request's own context.
func (rq *request) forward(ctx context.Context) {