Skip to content

fix: percent-encode caller-supplied identifiers in request paths - #44

Open
tas50 wants to merge 1 commit into
mainfrom
fix/escape-path-segments
Open

fix: percent-encode caller-supplied identifiers in request paths#44
tas50 wants to merge 1 commit into
mainfrom
fix/escape-path-segments

Conversation

@tas50

@tas50 tas50 commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Every service built its request path by concatenation, so a caller-supplied name reached the URL verbatim.

Why this is a bug

The v1.3 signature covers the canonical path, but net/http re-derives the wire path from the parsed *url.URL. Where those two derivations disagree, the request is signed one way and sent another:

name signed canonical path what the server receives
a b …/nodes/a b …/nodes/a%20b401, no useful error
a#b …/nodes/a#b …/nodes/a401 and wrong resource
ünïcode …/nodes/ünïcode …/nodes/%C3%BCn%C3%AFcode401
a%b cinc: build request: invalid URL escape "%b"

And a name containing / or .. escaped its collection entirely:

c.Nodes.Delete(ctx, "../clients/validator")
// emitted, correctly signed:
// DELETE /organizations/o/nodes/../clients/validator

Behind a proxy that normalises .. before erchef re-verifies the signature that lands as a 401; without normalisation it acts on a different object.

The fix

esc() (url.PathEscape) applied at every point an identifier enters a path, plus the org name in orgPath. Every identifier Chef considers legal is unreserved, so this is the identity function for valid input — no wire change, no behaviour change for correct callers.

Test plan

  • TestRequestPath_NameCannotEscapeItsCollection..%2Fclients%2Fvalidator stays one segment
  • TestRequestPath_SignatureCoversWirePath — re-verifies the RSA signature server-side against r.URL.EscapedPath() for plain, a b, a#b, a?b, a/b, a%b, ünïcode; all failed before this change except plain
  • TestOrgPath_EscapesOrgName
  • go vet ./... clean
  • go test ./... -race -count=2
  • cd integration && go test ./... (real signed wire protocol against cinc-zero)
  • No public surface change, so no README Status update needed

@tas50
tas50 force-pushed the fix/escape-path-segments branch 3 times, most recently from b52be71 to dc240a3 Compare September 8, 2026 16:53
Every service built its request path by string concatenation, so a name
reached the URL verbatim. Two things go wrong.

The v1.3 signature covers the canonical path, but net/http re-derives the
wire path from the parsed URL. A name Go encodes differently from what we
signed - a space, a non-ASCII rune - is signed one way and sent another,
and the server answers 401 with no hint as to why. A name containing "%"
did not even build a request.

Worse, a name containing "/" or ".." walked out of the collection its
service owns: Nodes.Delete(ctx, "../clients/validator") emitted a validly
signed DELETE against a path outside /nodes.

Escape each identifier as a single path segment at the point it enters the
path, and escape the org name in orgPath. Every identifier Chef itself
considers legal is unreserved, so this is the identity function for valid
input and nothing on the wire changes.

The new test verifies the signature server-side over the path that actually
arrived, which is the only way to catch a client that signs one path and
sends another.

Signed-off-by: Tim Smith <tim@mondoo.com>
@tas50
tas50 force-pushed the fix/escape-path-segments branch from dc240a3 to 1607d80 Compare September 10, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant