Skip to content

1.0.5 — the forwarded path keeps the characters it was sent with

Latest

Choose a tag to compare

@github-actions github-actions released this 23 Oct 05:27
· 2 commits to main since this release

1.0.4 opened TLS to post-quantum key exchange and made refused handshakes visible. 1.0.5 is one
forwarding fix, found in real traffic the day after that release: every /@username/ page 404'd
through the gateway while the same request direct to the origin returned 200.

What was happening

The gateway matches location on the decoded path (so /a%2fb cannot smuggle a segment, so
dot-segments collapse before they hit the filesystem), and it re-encodes that decoded path before
forwarding it. The re-encoder was ngx.escape_uri's set — unreserved characters only. Applied to a
path segment it escapes far more than the syntax requires:

client  ->  gateway  ->  origin
/@yinglink/            /%40yinglink/     404
/a+b                   /a%2Bb            wrong resource
/a:b                   /a%3Ab            wrong resource

@ is a legal pchar in RFC 3986. nginx forwards what the client sent; we were rewriting it. The
origin (and the WAF in front of it) keys user pages on the literal @, so homepages, avatars and
banner images all 404'd — and no amount of cache clearing or config juggling could show it, because
proxy_pass ...$request_uri goes through the same encoder.

The fix

Forwarding now encodes over pchar: unreserved (A-Za-z0-9 - . _ ~), sub-delims
(! $ & ' ( ) * + , ; =) and : @ pass through untouched, and exactly the bytes that must not
go raw are escaped — %, #, ?, space, control bytes and non-ASCII. % staying in the escaped
set is deliberate: the input is already decoded, so a literal % the client sent has to reach the
origin as %25, otherwise the origin decodes a second, different set of escapes (%2F into /,
%23 into #) and the two sides end up serving different resources — the same argument that makes
the upgrade path forward the raw target.

Both forward seams share the one helper now (the proxied request URI for h1/h2/h3 and the WebSocket
upgrade fallback request line), and so do the directory-listing hrefs, which were producing
/%40name/ links for files whose names start with @.

ngx.escape_uri / ngx.unescape_uri for Lua scripts keep their strict OpenResty semantics — that
set is for serializing query arguments, not for path segments.

Evidence

  • tools/uri_encode_e2e.py (new, and now in CI's functional-contract step) starts an origin that
    reports the raw request line it received and asserts on 14 shapes: 7/14 before the change,
    14/14 after.
    The seven that were red are exactly the @ : + , ; = & $ family.
  • proxy.rs unit test extended to the pchar round-trip; the %25 and #/? guards are asserted
    in both directions so this cannot silently regress.
  • Unit tests 504/504; full local regression TOTAL 97 scripts, nonzero=0 with SKIP empty
    (the roll went from 96 to 97 because uri_encode is now part of it); the packaged artefacts are
    checked by tools/release_verify.py driving the unpacked archive, not the build tree's binary.

Upgrading

Drop-in: replace the binary, nothing in the configuration changes. There is no new directive, no
new metric, and no behaviour change for any path that does not contain pchar characters the client
sent literally — paths with spaces, non-ASCII names, %-escapes and #/? go out exactly as they
did in 1.0.4.

onrouter -V
# ONRouter/1.0.5 (Rust, OpenResty-compatible)
#   crypto: rustls/ring [pq=off]

The eight platform archives below contain only onrouter and orr-gencert; hashes are in
SHA256SUMS.txt (sha256sum -c SHA256SUMS.txt). The "run it out of the box" bundle (conf, examples,
docs, generated demo certificate) is built on the release machine — see docs/OPERATIONS.md §2.1.