internal/proxy/request.go, forward and rewrite: httputil.ReverseProxy writes the path the app receives from URL.EscapedPath(). When the path as the client sent it holds a character Go's net/url escapes, such as |, ^, {, }, \ or a byte above 127, EscapedPath does not return that path. It decodes the whole path and escapes it again, so an encoded slash (%2F) reaches the app as /.
Repro, at the defaults, each request sent as written on a raw connection:
GET /a%2Fb|c HTTP/1.1: the app receives /a/b%7Cc.
GET /feat%2Fx/file^1 HTTP/1.1: the app receives /feat/x/file%5E1.
GET /x%2Fy/café HTTP/1.1, its UTF-8 bytes unescaped: the app receives /x/y/caf%C3%A9.
A test in internal/proxy that compares the app's r.RequestURI with the target sent fails on all three. Browsers send | and ^ in a path unescaped.
README, "What it does so far": "Passes each request to the app and the app's answer back unchanged: method, path, query, headers, body and status." An encoded slash turned into a slash can route the request to another resource in the app.
It also breaks what pathExempt relies on since #77, that the path as sent is the path the app receives. With SWWAF_RATE_LIMIT_EXEMPT_PATHS=/assets/, /assets/x%2Fy|z is exempt (a test of pathExempt shows it), though the README says a request "whose path as sent holds an encoded slash (%2F or %2f), is never exempt". The app receives /assets/x/y%7Cz there, so the exemption fits what the app gets, and the README's description does not.
Severity: minor. It needs an encoded reserved character and a character Go escapes in the same path, and it lets through or refuses nothing it should not.
Model: opus-5-5
`internal/proxy/request.go`, `forward` and `rewrite`: `httputil.ReverseProxy` writes the path the app receives from `URL.EscapedPath()`. When the path as the client sent it holds a character Go's `net/url` escapes, such as `|`, `^`, `{`, `}`, `\` or a byte above 127, `EscapedPath` does not return that path. It decodes the whole path and escapes it again, so an encoded slash (`%2F`) reaches the app as `/`.
Repro, at the defaults, each request sent as written on a raw connection:
- `GET /a%2Fb|c HTTP/1.1`: the app receives `/a/b%7Cc`.
- `GET /feat%2Fx/file^1 HTTP/1.1`: the app receives `/feat/x/file%5E1`.
- `GET /x%2Fy/café HTTP/1.1`, its UTF-8 bytes unescaped: the app receives `/x/y/caf%C3%A9`.
A test in `internal/proxy` that compares the app's `r.RequestURI` with the target sent fails on all three. Browsers send `|` and `^` in a path unescaped.
README, "What it does so far": "Passes each request to the app and the app's answer back unchanged: method, path, query, headers, body and status." An encoded slash turned into a slash can route the request to another resource in the app.
It also breaks what `pathExempt` relies on since https://git.eeqj.de/sneak/smallwebwaf/issues/77, that the path as sent is the path the app receives. With `SWWAF_RATE_LIMIT_EXEMPT_PATHS=/assets/`, `/assets/x%2Fy|z` is exempt (a test of `pathExempt` shows it), though the README says a request "whose path as sent holds an encoded slash (`%2F` or `%2f`), is never exempt". The app receives `/assets/x/y%7Cz` there, so the exemption fits what the app gets, and the README's description does not.
Severity: minor. It needs an encoded reserved character and a character Go escapes in the same path, and it lets through or refuses nothing it should not.
Model: opus-5-5
rewrite sends the app the path exactly as the client sent it, taken from the request line, so EscapedPath no longer re-encodes it: /a%2Fb|c reaches the app as /a%2Fb|c. The query is already passed as sent.
pathExempt, the log line's path, and anything else that relies on "the path as sent is the path the app receives" use that same path, so /assets/x%2Fy|z is never exempt, as README.md says.
A test in internal/proxy comparing the app's r.RequestURI with the target sent, for the issue's three requests, fails before and passes after; a test of pathExempt covers /assets/x%2Fy|z.
Done when those tests pass and an independent reviewer passes the PR. It changes internal/proxy/request.go beside #147, in other functions; whichever lands second is rebased onto the first.
Model: opus-5-5
Plan, one PR to `next`:
- `rewrite` sends the app the path exactly as the client sent it, taken from the request line, so `EscapedPath` no longer re-encodes it: `/a%2Fb|c` reaches the app as `/a%2Fb|c`. The query is already passed as sent.
- `pathExempt`, the log line's `path`, and anything else that relies on "the path as sent is the path the app receives" use that same path, so `/assets/x%2Fy|z` is never exempt, as `README.md` says.
- A test in `internal/proxy` comparing the app's `r.RequestURI` with the target sent, for the issue's three requests, fails before and passes after; a test of `pathExempt` covers `/assets/x%2Fy|z`.
Done when those tests pass and an independent reviewer passes the PR. It changes `internal/proxy/request.go` beside https://git.eeqj.de/sneak/smallwebwaf/pulls/147, in other functions; whichever lands second is rebased onto the first.
Model: opus-5-5
clawbot
self-assigned this 2026-10-20 15:29:05 +02:00
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.
internal/proxy/request.go,forwardandrewrite:httputil.ReverseProxywrites the path the app receives fromURL.EscapedPath(). When the path as the client sent it holds a character Go'snet/urlescapes, such as|,^,{,},\or a byte above 127,EscapedPathdoes not return that path. It decodes the whole path and escapes it again, so an encoded slash (%2F) reaches the app as/.Repro, at the defaults, each request sent as written on a raw connection:
GET /a%2Fb|c HTTP/1.1: the app receives/a/b%7Cc.GET /feat%2Fx/file^1 HTTP/1.1: the app receives/feat/x/file%5E1.GET /x%2Fy/café HTTP/1.1, its UTF-8 bytes unescaped: the app receives/x/y/caf%C3%A9.A test in
internal/proxythat compares the app'sr.RequestURIwith the target sent fails on all three. Browsers send|and^in a path unescaped.README, "What it does so far": "Passes each request to the app and the app's answer back unchanged: method, path, query, headers, body and status." An encoded slash turned into a slash can route the request to another resource in the app.
It also breaks what
pathExemptrelies on since #77, that the path as sent is the path the app receives. WithSWWAF_RATE_LIMIT_EXEMPT_PATHS=/assets/,/assets/x%2Fy|zis exempt (a test ofpathExemptshows it), though the README says a request "whose path as sent holds an encoded slash (%2For%2f), is never exempt". The app receives/assets/x/y%7Czthere, so the exemption fits what the app gets, and the README's description does not.Severity: minor. It needs an encoded reserved character and a character Go escapes in the same path, and it lets through or refuses nothing it should not.
Model: opus-5-5
Plan, one PR to
next:rewritesends the app the path exactly as the client sent it, taken from the request line, soEscapedPathno longer re-encodes it:/a%2Fb|creaches the app as/a%2Fb|c. The query is already passed as sent.pathExempt, the log line'spath, and anything else that relies on "the path as sent is the path the app receives" use that same path, so/assets/x%2Fy|zis never exempt, asREADME.mdsays.internal/proxycomparing the app'sr.RequestURIwith the target sent, for the issue's three requests, fails before and passes after; a test ofpathExemptcovers/assets/x%2Fy|z.Done when those tests pass and an independent reviewer passes the PR. It changes
internal/proxy/request.gobeside #147, in other functions; whichever lands second is rebased onto the first.Model: opus-5-5