Leave SWWAF_RATE_LIMIT_EXEMPT_PATHS out of the request rate limits #80

Merged
clawbot merged 1 commits from issue-77-exempt-paths into next 2026-10-06 18:47:14 +02:00
Collaborator

Adds SWWAF_RATE_LIMIT_EXEMPT_PATHS for #77: a comma-separated list of path prefixes, empty by default. A request whose path as sent, the path the app receives, before the query string and not percent-decoded, 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. A prefix not starting with / stops the start.

  • As the corrections on the issue rule, 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, and on /assets%2Fx as one path segment.
  • Matching the path as sent keeps /%61ssets/x counted, since a router that matches the path as received serves it outside /assets/.
  • The path is not resolved: /assets/ matches /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; observe mode never logs such a request 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 the app receives; it differs from the client's bytes only when Go re-encodes a path it cannot pass on as sent.

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 as sent, the path the app receives, before the query string and not percent-decoded, 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. A prefix not starting with `/` stops the start. - As the corrections on the issue rule, 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`, and on `/assets%2Fx` as one path segment. - Matching the path as sent keeps `/%61ssets/x` counted, since a router that matches the path as received serves it outside `/assets/`. - The path is not resolved: `/assets/` matches `/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; `observe` mode never logs such a request 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 the app receives; it differs from the client's bytes only when Go re-encodes a path it cannot pass on as sent. 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 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
clawbot force-pushed issue-77-exempt-paths from fc469d96a9 to c26fffca1e 2026-10-06 15:47:32 +02:00 Compare
Author
Collaborator

#80 (comment): prefixes are now compared with the path as sent (URL.EscapedPath()), with the .., backslash and encoded-slash checks unchanged; a test sees /%61ssets/x counted, and README.md, the code comment and the commit message state that path.

Model: opus-5-5

https://git.eeqj.de/sneak/smallwebwaf/pulls/80#issuecomment-129716: prefixes are now compared with the path as sent (`URL.EscapedPath()`), with the `..`, backslash and encoded-slash checks unchanged; a test sees `/%61ssets/x` counted, and `README.md`, the code comment and the commit message state that path. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-06 15:48:24 +02:00
Author
Collaborator

Review failed: needs rework.

  1. internal/proxy/ratelimits_test.go, TestRateLimitCountsPathsThatAreNotExempt: no test fails with the encoded-slash rule broken. /assets%2Fx and /assets%2fx do not start with /assets/ as sent, so they are counted whether or not pathExempt checks for %2F and %2f; with that check removed, or made case-sensitive, every test still passes, while the rule on #77 makes a request whose path as sent holds an encoded slash never exempt. Acceptable: the test also sends paths under the prefix that hold an encoded slash, such as /assets/x%2Fy and /assets/x%2fy, and sees them counted, with the comment above those cases saying what they show.

Judgement calls:

  • A prefix holding a character the path as sent always carries percent-encoded, such as a raw space or é, is accepted at start and never matches; not raised, since such requests are only counted, never wrongly exempt.
  • An app that takes its path from a request header such as X-Original-URL could serve an exempt request as another path; not raised, like the double-encoded paths already accepted.

Model: opus-5-5

Review failed: needs rework. 1. `internal/proxy/ratelimits_test.go`, `TestRateLimitCountsPathsThatAreNotExempt`: no test fails with the encoded-slash rule broken. `/assets%2Fx` and `/assets%2fx` do not start with `/assets/` as sent, so they are counted whether or not `pathExempt` checks for `%2F` and `%2f`; with that check removed, or made case-sensitive, every test still passes, while the rule on https://git.eeqj.de/sneak/smallwebwaf/issues/77 makes a request whose path as sent holds an encoded slash never exempt. Acceptable: the test also sends paths under the prefix that hold an encoded slash, such as `/assets/x%2Fy` and `/assets/x%2fy`, and sees them counted, with the comment above those cases saying what they show. Judgement calls: - A prefix holding a character the path as sent always carries percent-encoded, such as a raw space or `é`, is accepted at start and never matches; not raised, since such requests are only counted, never wrongly exempt. - An app that takes its path from a request header such as `X-Original-URL` could serve an exempt request as another path; not raised, like the double-encoded paths already accepted. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-06 16:18:43 +02:00
clawbot added 1 commit 2026-10-06 18:28:07 +02:00
A request is neither counted nor refused by the request rate limits
when its path as sent, the path the app receives, not percent-decoded,
starts with one of the comma-separated prefixes in
SWWAF_RATE_LIMIT_EXEMPT_PATHS, so /%61ssets/x is not under /assets/. 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, and
its log line has no counts. 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 c26fffca1e to c12b901274 2026-10-06 18:28:07 +02:00 Compare
Author
Collaborator

#80 (comment): TestRateLimitCountsPathsThatAreNotExempt now sends /assets/x%2Fy and /assets/x%2fy, under a comment saying they are under the prefix and counted for their encoded slash alone. Every other check in pathExempt already had a case that fails without it, so no other case was added.
Rebased onto next after #82: the TODO no longer lists exemptions; README.md and the exempt-path test now say such a request's log line has no counts; two rows of TestInvalidValueStopsTheStart now share a line, which keeps the function within the linter's length limit.

Model: opus-5-5

https://git.eeqj.de/sneak/smallwebwaf/pulls/80#issuecomment-129895: `TestRateLimitCountsPathsThatAreNotExempt` now sends `/assets/x%2Fy` and `/assets/x%2fy`, under a comment saying they are under the prefix and counted for their encoded slash alone. Every other check in `pathExempt` already had a case that fails without it, so no other case was added. Rebased onto `next` after https://git.eeqj.de/sneak/smallwebwaf/pulls/82: the TODO no longer lists exemptions; `README.md` and the exempt-path test now say such a request's log line has no `counts`; two rows of `TestInvalidValueStopsTheStart` now share a line, which keeps the function within the linter's length limit. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-06 18:28:25 +02:00
Author
Collaborator

Review passed.

  • Judgement call: two rows of TestInvalidValueStopsTheStart on one line accepted as plain, since that table already puts several rows for one setting on a line.
  • Judgement call: README.md does not say the prefixes are separated by commas, as it says this for none of its list settings; not raised.

Model: opus-5-5

Review passed. - Judgement call: two rows of `TestInvalidValueStopsTheStart` on one line accepted as plain, since that table already puts several rows for one setting on a line. - Judgement call: `README.md` does not say the prefixes are separated by commas, as it says this for none of its list settings; not raised. Model: opus-5-5
clawbot merged commit 74bdc6a449 into next 2026-10-06 18:47:14 +02:00
clawbot deleted branch issue-77-exempt-paths 2026-10-06 18:47:14 +02:00
Sign in to join this conversation.