Treat a path parameter as a segment, not as URL syntax - #15
Conversation
630a463 to
6ddde6d
Compare
Path parameters are substituted into a template — `…/{username}/{style_id}/
static/{overlay}/…` — and the value went in unencoded, so one carrying URL
syntax changed the shape of the request rather than naming a segment within
it. A `?` appended query parameters the caller never asked for, `..` and its
backslash spelling moved the path, and `#` truncated it. Each went out with
the caller's token and the command's method.
The host was never reachable: `//other`, `https://other`, `%2f%2f…` and
`x@other` all still resolve to the API's own host, so nothing could be
directed at another server. What was reachable was a different path on the
API — worst shaped as `styles delete`, a `DELETE` whose target was
influenced by its argument. It matters most where the value does not come
from the person typing: an agent building a command from a prompt, or a
script interpolating a variable from upstream.
`/`, `?`, `#` and `\` are now percent-encoded. `\` is in that list because a
special scheme treats it as a separator too, which `reqwest::Url` confirms —
encoding `/` alone would have left it open.
**Only those four, deliberately.** These values carry punctuation on purpose:
`static get-image`'s template is
`…/static/{overlay}/{lon},{lat},{zoom},{bearing},{pitch}/{width}x{height}{highRes}{format}`,
where the overlay is `pin-s+f74e4e(-122.46,37.77)`, `{highRes}` is `@2x` and
three placeholders share one comma-separated segment. Encoding everything
outside RFC 3986's unreserved set would rewrite all of that and risk breaking
requests that work today. Encoding only what alters the URL's shape cannot
change a request that does not already contain those characters — pinned by a
test that walks real values and asserts they pass through byte for byte, and
without allocating.
A value of `.` or `..` is **refused** rather than encoded, because encoding
does not stop it: a special scheme reads `%2e%2e` as a double-dot segment
too, so it climbs however it is spelled. Encoding the separators handles the
multi-level case by collapsing it to one segment; this handles the remainder.
Eight tests, including that an empty optional parameter still drops its whole
segment — several operations rely on that and it had to survive.
595 tests, fmt and clippy clean.
6ddde6d to
a9bb1c2
Compare
|
Found 1 issue:
Lines 1122 to 1139 in a9bb1c2 |
|
Thanks — the observation is accurate, the conclusion isn't, and I checked both rather than assuming either way. Accurate: Not accurate: that it becomes Compare the case this PR does fix, where the dots are encoded but the slashes are raw — there the parser does traverse, which is why So the value goes on the wire as a single segment. It would only traverse if a server decoded And blocking it would break documented behaviour. Percent-encoded values in path parameters are a Mapbox feature, not an attack signature. From the static-images spec in this repo:
A custom marker overlay is On test coverage: the suggestion to exercise encoded dots and slashes together is a fair one, so I've added it — asserting the documented behaviour (single segment, no traversal) rather than a block, so the guarantee is pinned rather than assumed. |
Raised in review: `%2e%2e%2fvictim` has no raw structural character, so it passes through unchanged. True — and it names one segment rather than traversing, because the URL parser never decodes `%2f` into a separator. Encoded dots with *raw* slashes are the case that does resolve, and the encoding this adds is what stops it; the two were only distinguishable by reading the parser, so now a test says which is which. Left alone rather than blocked on purpose. Percent-encoded values in path parameters are a documented Mapbox feature: a custom marker overlay is `url-https%3A%2F%2Fexample.com%2Fmarker.png(…)`, `geojson(…)` takes URI-encoded GeoJSON, and the spec says a bbox's brackets may be sent "literally or percent-encoded as %5B". Refusing them would break all three.
zmofei
left a comment
There was a problem hiding this comment.
Percent-encoding argument checks out — %2F stays a single path segment at the URL-parsing level, so this doesn't reopen the traversal the PR fixes. LGTM.
Carries the path-parameter encoding fix (#15) that's been sitting on this branch since it was cut, along with the CI timeout fix (#14), the private-repo doc de-linking (#16) and update-check version validation (#18) — none of which had reached main. See CHANGELOG.md for what 0.2.1 actually changes user-facing behavior.
Path parameters are substituted into a URL template —
…/{username}/{style_id}/static/{overlay}/…— and the value went in unencoded. A value carrying URL syntax therefore changed the shape of the request instead of naming a segment within it.What changes
/,?,#and\in a path parameter are now percent-encoded, so they name a segment. A value of.or..is refused asinvalid_path_parameter.\is in that list because a special scheme treats it as a path separator too —reqwest::Urlconfirms it — so encoding/alone would have left an equivalent route open../..are refused rather than encoded, because encoding doesn't stop them: a special scheme reads%2e%2eas a double-dot segment as well, so such a value climbs however it's spelled. Encoding the separators handles the multi-level case by collapsing it into a single segment; the refusal handles what's left.Only those four characters, deliberately
These values carry punctuation on purpose.
static get-image's template alone is:— where the overlay is
pin-s+f74e4e(-122.46,37.77),{highRes}is@2x,{format}is.png, and three placeholders share one comma-separated segment.Percent-encoding everything outside RFC 3986's unreserved set would rewrite all of that, and risk breaking requests that work today. Encoding only what alters the URL's shape cannot change any request that doesn't already contain those four characters — pinned by a test that walks real values (overlay,
@2x,.png,mapbox.mapbox-streets-v8,Arial Unicode MS Regular,0-255) and asserts each passes through byte for byte, and without allocating.Scope
The host was never reachable —
//other,https://other,%2f%2f…andx@otherall still resolve to the API's own host, so nothing could be directed at another server. What was reachable was a different path on the API, with the caller's token and the command's method.It matters most where the value doesn't come from the person typing: an agent building a command from a prompt, or a script interpolating a variable from upstream. For someone typing their own argument it was a footgun rather than a risk.
Tests
Eight, including that an empty optional parameter still drops its whole segment — several operations depend on that and it had to survive the change.
595 tests,
cargo fmt --checkandcargo clippy --locked --all-targetsclean.docs/commands.mdgains the new error code.