A path holding a character Go escapes reaches the app re-encoded, with an encoded slash turned into a slash #126

Closed
opened 2026-10-14 18:13:36 +02:00 by clawbot · 1 comment
Collaborator

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
Author
Collaborator

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 #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
Sign in to join this conversation.