Leave SWWAF_RATE_LIMIT_EXEMPT_PATHS out of the request rate limits #80

Open
clawbot wants to merge 1 commits from issue-77-exempt-paths into next
Collaborator

Adds SWWAF_RATE_LIMIT_EXEMPT_PATHS for #77: a comma-separated list of path prefixes, empty by default. A request whose path, percent-decoded and before the query string, starts with one of them is neither counted nor refused by the request rate limits; SWWAF_DENY_NETS, bans and the country lists still refuse it, and its log line and metrics show it as forward with no limit_hit. A prefix that does not start with / stops the start with a message naming the setting.

  • As the second correction on the issue rules, a request whose decoded path contains .. anywhere or a backslash, or whose path as sent holds %2F or %2f, is never exempt: an app may act on /assets/..%2Flogin as /login, on /assets/..;/login the same way once it drops ; parameters, and on /assets%2Fx as one path segment.
  • The path is not resolved: . segments and repeated slashes stay, so /assets/ matches /assets/ itself and /assets//x, but not //assets/x.
  • No wildcards, case counts, and a prefix matches only at the start.
  • The exemption joins SWWAF_RATE_LIMIT_EXEMPT_NETS in checkClient, at the rate-limit step only; in observe mode such a request is never logged as one the rate limits would refuse.

Disclosures:

  • Judgement call: the backslash check is on the decoded path, so an encoded backslash (%5C) is never exempt either.
  • Judgement call: the path as sent is read as URL.EscapedPath(), the path as the app receives it; it differs from the client's bytes only when Go re-encodes a path it cannot pass on as sent, and then the app receives a real slash.

Model: opus-5-5

Adds `SWWAF_RATE_LIMIT_EXEMPT_PATHS` for https://git.eeqj.de/sneak/smallwebwaf/issues/77: a comma-separated list of path prefixes, empty by default. A request whose path, percent-decoded and before the query string, starts with one of them is neither counted nor refused by the request rate limits; `SWWAF_DENY_NETS`, bans and the country lists still refuse it, and its log line and metrics show it as `forward` with no `limit_hit`. A prefix that does not start with `/` stops the start with a message naming the setting. - As the second correction on the issue rules, a request whose decoded path contains `..` anywhere or a backslash, or whose path as sent holds `%2F` or `%2f`, is never exempt: an app may act on `/assets/..%2Flogin` as `/login`, on `/assets/..;/login` the same way once it drops `;` parameters, and on `/assets%2Fx` as one path segment. - The path is not resolved: `.` segments and repeated slashes stay, so `/assets/` matches `/assets/` itself and `/assets//x`, but not `//assets/x`. - No wildcards, case counts, and a prefix matches only at the start. - The exemption joins `SWWAF_RATE_LIMIT_EXEMPT_NETS` in `checkClient`, at the rate-limit step only; in `observe` mode such a request is never logged as one the rate limits would refuse. Disclosures: - Judgement call: the backslash check is on the decoded path, so an encoded backslash (`%5C`) is never exempt either. - Judgement call: the path as sent is read as `URL.EscapedPath()`, the path as the app receives it; it differs from the client's bytes only when Go re-encodes a path it cannot pass on as sent, and then the app receives a real slash. Model: opus-5-5
clawbot added the needs-review label 2026-10-06 12:07:04 +02:00
clawbot self-assigned this 2026-10-06 12:07:04 +02:00
Author
Collaborator

Review failed: needs rework.

  1. internal/proxy/request.go, check: a client can escape the rate limits through an exempt prefix. The prefix is compared with the path as sent, and that same path goes to the app unchanged, so with /assets/ set, /assets/..%2Flogin (and /assets/%2e%2e/login, /assets/../login) is never counted, while an app that decodes and resolves the path, as nginx does, serves /login. traefik's default cleaning removes .. segments, written plainly or as %2e%2e, but leaves ..%2F as it is, and a request that reaches smallwebwaf without that cleaning gets all three through. Acceptable: exempt a request only when the path the app will act on starts with the prefix, that is the percent-decoded path with its . and .. segments and repeated slashes resolved, or never exempt a path whose decoded form holds a . or .. segment; tests that see those three paths counted; README.md, the code comment and the commit message describing the path that is matched. (Today's "as the client sent it, character for character" is also not what the log's path holds for characters Go encodes again, such as non-ASCII ones.)
  2. internal/proxy/ratelimits_test.go: no test shows the country lists still refusing a path under a prefix. It needs neither GeoJS nor the real clock: load the client's country with server.GeoJS.Load before the request, as metrics_test.go does. Acceptable: a test in which a client from a denied country asking for a path under a prefix is refused with country_denied, and the PR body's disclosure removed.
  3. internal/proxy/ratelimits_test.go: no test shows that a prefix matches only at the start of the path. Acceptable: the test also sends a path that holds a prefix after its start, such as /static/assets/app.js, which README.md says is not matched, and sees it counted.

Judgement call: finding 1 departs from the issue's "path as received", since that matching lets a client escape the limits for paths the app serves outside the prefix.

Model: opus-5-5

Review failed: needs rework. 1. `internal/proxy/request.go`, `check`: a client can escape the rate limits through an exempt prefix. The prefix is compared with the path as sent, and that same path goes to the app unchanged, so with `/assets/` set, `/assets/..%2Flogin` (and `/assets/%2e%2e/login`, `/assets/../login`) is never counted, while an app that decodes and resolves the path, as nginx does, serves `/login`. traefik's default cleaning removes `..` segments, written plainly or as `%2e%2e`, but leaves `..%2F` as it is, and a request that reaches `smallwebwaf` without that cleaning gets all three through. Acceptable: exempt a request only when the path the app will act on starts with the prefix, that is the percent-decoded path with its `.` and `..` segments and repeated slashes resolved, or never exempt a path whose decoded form holds a `.` or `..` segment; tests that see those three paths counted; `README.md`, the code comment and the commit message describing the path that is matched. (Today's "as the client sent it, character for character" is also not what the log's `path` holds for characters Go encodes again, such as non-ASCII ones.) 2. `internal/proxy/ratelimits_test.go`: no test shows the country lists still refusing a path under a prefix. It needs neither GeoJS nor the real clock: load the client's country with `server.GeoJS.Load` before the request, as `metrics_test.go` does. Acceptable: a test in which a client from a denied country asking for a path under a prefix is refused with `country_denied`, and the PR body's disclosure removed. 3. `internal/proxy/ratelimits_test.go`: no test shows that a prefix matches only at the start of the path. Acceptable: the test also sends a path that holds a prefix after its start, such as `/static/assets/app.js`, which `README.md` says is not matched, and sees it counted. Judgement call: finding 1 departs from the issue's "path as received", since that matching lets a client escape the limits for paths the app serves outside the prefix. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-06 12:36:37 +02:00
clawbot force-pushed issue-77-exempt-paths from 611ae5c008 to 890dcedfd7 2026-10-06 13:24:18 +02:00 Compare
Author
Collaborator

Rework of #80 (comment):

  1. Fixed: prefixes are matched against path.Clean of the percent-decoded URL.Path; /assets/../login, /assets/%2e%2e/login and /assets/..%2Flogin are tested counted, and README.md, the code comment and the commit message describe that path.
  2. Fixed: the exempt-path test loads the client's country with server.GeoJS.Load and sees /assets/app.js refused with country_denied; the PR body's disclosure is gone.
  3. Fixed: /static/assets/app.js is tested counted.

Rebased onto next: with observe mode landed, the exemption now sits in checkClient.

Model: opus-5-5

Rework of https://git.eeqj.de/sneak/smallwebwaf/pulls/80#issuecomment-129353: 1. Fixed: prefixes are matched against `path.Clean` of the percent-decoded `URL.Path`; `/assets/../login`, `/assets/%2e%2e/login` and `/assets/..%2Flogin` are tested counted, and `README.md`, the code comment and the commit message describe that path. 2. Fixed: the exempt-path test loads the client's country with `server.GeoJS.Load` and sees `/assets/app.js` refused with `country_denied`; the PR body's disclosure is gone. 3. Fixed: `/static/assets/app.js` is tested counted. Rebased onto `next`: with `observe` mode landed, the exemption now sits in `checkClient`. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-06 13:24:32 +02:00
Author
Collaborator

Review failed: needs rework.

  1. internal/proxy/request.go, checkClient: a client can still escape the rate limits through an exempt prefix. path.Clean of the percent-decoded path takes an encoded slash (%2F) for a slash and resolves .. across it, but an app that keeps %2F inside a path segment, as Go's own router and gitea's do, does not. With /assets/ set, /sneak/app/src/branch/main/..%2F..%2F..%2F..%2F..%2F..%2Fassets/x is matched as /assets/x and never counted, while the app's route for /{owner}/{repo}/src/... serves it; /assets%2Fx reaches a one-segment route such as /{page} the same way. traefik's defaults let %2F through, and its cleaning leaves ..%2F as it is. The disclosed /assets/..;/login, which an app that drops ; parameters (as Java servers do) serves as /login, is the same escape. Acceptable: count every request whose percent-decoded path contains .. anywhere, or whose path as sent holds an encoded slash, and match the rest as now; tests that see such paths counted; README.md and the code comment stating the rule (/static/../assets/app.js then no longer matches).
  2. README.md, TODO: it still lists exemptions as milestone 3 work to come, while the status paragraph says the paths the rate limits do not count are built, and #77 is the build order's exemptions. Acceptable: the TODO no longer names exemptions.

Judgement calls:

  • Finding 1 goes past the matching rule in #77 (comment), since that rule still lets a client escape the limits for apps that keep %2F inside a segment.
  • Plain prefixes accepted, as the issue asks: /assets also matches /assetsX, and / exempts every path.
  • No test covers the exemption in observe mode; the definition of done does not ask for one.

Model: opus-5-5

Review failed: needs rework. 1. `internal/proxy/request.go`, `checkClient`: a client can still escape the rate limits through an exempt prefix. `path.Clean` of the percent-decoded path takes an encoded slash (`%2F`) for a slash and resolves `..` across it, but an app that keeps `%2F` inside a path segment, as Go's own router and gitea's do, does not. With `/assets/` set, `/sneak/app/src/branch/main/..%2F..%2F..%2F..%2F..%2F..%2Fassets/x` is matched as `/assets/x` and never counted, while the app's route for `/{owner}/{repo}/src/...` serves it; `/assets%2Fx` reaches a one-segment route such as `/{page}` the same way. traefik's defaults let `%2F` through, and its cleaning leaves `..%2F` as it is. The disclosed `/assets/..;/login`, which an app that drops `;` parameters (as Java servers do) serves as `/login`, is the same escape. Acceptable: count every request whose percent-decoded path contains `..` anywhere, or whose path as sent holds an encoded slash, and match the rest as now; tests that see such paths counted; `README.md` and the code comment stating the rule (`/static/../assets/app.js` then no longer matches). 2. `README.md`, TODO: it still lists exemptions as milestone 3 work to come, while the status paragraph says the paths the rate limits do not count are built, and https://git.eeqj.de/sneak/smallwebwaf/issues/77 is the build order's exemptions. Acceptable: the TODO no longer names exemptions. Judgement calls: - Finding 1 goes past the matching rule in https://git.eeqj.de/sneak/smallwebwaf/issues/77#issuecomment-129359, since that rule still lets a client escape the limits for apps that keep `%2F` inside a segment. - Plain prefixes accepted, as the issue asks: `/assets` also matches `/assetsX`, and `/` exempts every path. - No test covers the exemption in `observe` mode; the definition of done does not ask for one. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-06 13:44:30 +02:00
clawbot force-pushed issue-77-exempt-paths from 890dcedfd7 to ae5445f70f 2026-10-06 14:13:02 +02:00 Compare
clawbot added 1 commit 2026-10-06 14:38:39 +02:00
A request is neither counted nor refused by the request rate limits
when its path, percent-decoded, starts with one of the comma-separated
prefixes in SWWAF_RATE_LIMIT_EXEMPT_PATHS. A request whose decoded path
contains .. or a backslash, or whose path as sent holds an encoded
slash, is never exempt, since an app may act on it as a path outside
every prefix, such as /assets/..%2Flogin as /login. 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.

Model: opus-5-5
clawbot force-pushed issue-77-exempt-paths from ae5445f70f to fc469d96a9 2026-10-06 14:38:39 +02:00 Compare
Author
Collaborator

Rework for #80 (comment):

  1. Fixed, following #77 (comment); the path is no longer resolved, and a test sees each named path counted.
  2. Fixed.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/smallwebwaf/pulls/80#issuecomment-129508: 1. Fixed, following https://git.eeqj.de/sneak/smallwebwaf/issues/77#issuecomment-129519; the path is no longer resolved, and a test sees each named path counted. 2. Fixed. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-06 14:38:50 +02:00
Author
Collaborator

Review failed: needs rework.

  1. internal/proxy/request.go, pathExempt: a client can still escape the rate limits through an exempt prefix, by percent-encoding a character of the prefix itself. The prefix is compared with the percent-decoded path, while the app receives the path as sent: with /assets/ set, /%61ssets/x is never counted and reaches the app as /%61ssets/x, which a router that matches the path as received (chi, which gitea uses, and Express) serves from a route outside /assets/, such as gitea's /{username}/{reponame}. traefik decodes such characters only from v3.4.1, and a request that reaches smallwebwaf without traefik is not decoded at all. Acceptable: compare the prefixes with the path as sent (URL.EscapedPath(), the path the app receives), keeping the checks for .., backslashes and encoded slashes as they are; a test that sees /%61ssets/x counted; README.md ("percent-decoded ... starts with a prefix", "A prefix is written without percent-encoding"), the code comment and the commit message stating that path.

Judgement calls:

  • Double-encoded paths such as /assets/%252e%252e/login stay exempt: only an app that decodes a path twice acts on them as a path outside the prefix.
  • "Path as sent" read as the path the app receives (URL.EscapedPath()), as the PR body discloses.
  • The PR body, at about 260 words, accepted as within about 250.

Model: opus-5-5

Review failed: needs rework. 1. `internal/proxy/request.go`, `pathExempt`: a client can still escape the rate limits through an exempt prefix, by percent-encoding a character of the prefix itself. The prefix is compared with the percent-decoded path, while the app receives the path as sent: with `/assets/` set, `/%61ssets/x` is never counted and reaches the app as `/%61ssets/x`, which a router that matches the path as received (chi, which gitea uses, and Express) serves from a route outside `/assets/`, such as gitea's `/{username}/{reponame}`. traefik decodes such characters only from v3.4.1, and a request that reaches `smallwebwaf` without traefik is not decoded at all. Acceptable: compare the prefixes with the path as sent (`URL.EscapedPath()`, the path the app receives), keeping the checks for `..`, backslashes and encoded slashes as they are; a test that sees `/%61ssets/x` counted; `README.md` ("percent-decoded ... starts with a prefix", "A prefix is written without percent-encoding"), the code comment and the commit message stating that path. Judgement calls: - Double-encoded paths such as `/assets/%252e%252e/login` stay exempt: only an app that decodes a path twice acts on them as a path outside the prefix. - "Path as sent" read as the path the app receives (`URL.EscapedPath()`), as the PR body discloses. - The PR body, at about 260 words, accepted as within about 250. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-06 14:51:16 +02:00
Some checks are pending
check / check (push) Waiting to run
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin issue-77-exempt-paths:issue-77-exempt-paths
git checkout issue-77-exempt-paths
Sign in to join this conversation.