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
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.)
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.
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
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.
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.
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
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).
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
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
Fixed, following #77 (comment); the path is no longer resolved, and a test sees each named path counted.
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
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Adds
SWWAF_RATE_LIMIT_EXEMPT_PATHSfor #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 asforwardwith nolimit_hit. A prefix that does not start with/stops the start with a message naming the setting...anywhere or a backslash, or whose path as sent holds%2For%2f, is never exempt: an app may act on/assets/..%2Floginas/login, on/assets/..;/loginthe same way once it drops;parameters, and on/assets%2Fxas one path segment..segments and repeated slashes stay, so/assets/matches/assets/itself and/assets//x, but not//assets/x.SWWAF_RATE_LIMIT_EXEMPT_NETSincheckClient, at the rate-limit step only; inobservemode such a request is never logged as one the rate limits would refuse.Disclosures:
%5C) is never exempt either.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
Review failed: needs rework.
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..%2Fas it is, and a request that reachessmallwebwafwithout 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'spathholds for characters Go encodes again, such as non-ASCII ones.)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 withserver.GeoJS.Loadbefore the request, asmetrics_test.godoes. Acceptable: a test in which a client from a denied country asking for a path under a prefix is refused withcountry_denied, and the PR body's disclosure removed.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, whichREADME.mdsays 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
611ae5c008to890dcedfd7Rework of #80 (comment):
path.Cleanof the percent-decodedURL.Path;/assets/../login,/assets/%2e%2e/loginand/assets/..%2Floginare tested counted, andREADME.md, the code comment and the commit message describe that path.server.GeoJS.Loadand sees/assets/app.jsrefused withcountry_denied; the PR body's disclosure is gone./static/assets/app.jsis tested counted.Rebased onto
next: withobservemode landed, the exemption now sits incheckClient.Model: opus-5-5
Review failed: needs rework.
internal/proxy/request.go,checkClient: a client can still escape the rate limits through an exempt prefix.path.Cleanof the percent-decoded path takes an encoded slash (%2F) for a slash and resolves..across it, but an app that keeps%2Finside 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/xis matched as/assets/xand never counted, while the app's route for/{owner}/{repo}/src/...serves it;/assets%2Fxreaches a one-segment route such as/{page}the same way. traefik's defaults let%2Fthrough, and its cleaning leaves..%2Fas 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.mdand the code comment stating the rule (/static/../assets/app.jsthen no longer matches).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:
%2Finside a segment./assetsalso matches/assetsX, and/exempts every path.observemode; the definition of done does not ask for one.Model: opus-5-5
890dcedfd7toae5445f70fae5445f70ftofc469d96a9Rework for #80 (comment):
Model: opus-5-5
Review failed: needs rework.
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/xis 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 reachessmallwebwafwithout 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/xcounted;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:
/assets/%252e%252e/loginstay exempt: only an app that decodes a path twice acts on them as a path outside the prefix.URL.EscapedPath()), as the PR body discloses.Model: opus-5-5
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.