From 615166874bfbae382123522b352b4969cc9f8b21 Mon Sep 17 00:00:00 2001 From: kilyanni Date: Fri, 10 Jul 2026 14:48:02 +0200 Subject: [PATCH] fix: decode request path before SigV4 canonicalization --- src/service.rs | 12 ++++++++++-- src/signature_v4.rs | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/service.rs b/src/service.rs index 6616839..b3a5090 100644 --- a/src/service.rs +++ b/src/service.rs @@ -489,9 +489,13 @@ async fn check_presigned_url( .headers .map_signed_headers(&presigned_url.signed_headers); + // Canonicalize the decoded path, the way routing does (decode_uri_path). + // The client signs uri_encode(key) exactly once, so feeding the raw wire + // path would encode a second time ('+' -> %252B) and never match. + let decoded_path = decode_uri_path(ctx.req)?; let canonical_request = signature_v4::create_presigned_canonical_request( ctx.req.method(), - ctx.req.uri().path(), + decoded_path.as_ref(), qs.as_ref(), &headers, ); @@ -544,7 +548,11 @@ async fn check_header_auth( let signature = { let method = ctx.req.method(); - let uri_path = ctx.req.uri().path(); + // Canonicalize the decoded path, the way routing does (decode_uri_path). + // The client signs uri_encode(key) exactly once, so feeding the raw wire + // path would encode a second time ('+' -> %252B) and never match. + let decoded_path = decode_uri_path(ctx.req)?; + let uri_path: &str = decoded_path.as_ref(); let query_strings: &[(String, String)] = ctx.query_strings.as_ref().map_or(&[], AsRef::as_ref); diff --git a/src/signature_v4.rs b/src/signature_v4.rs index a745b49..89eecf8 100644 --- a/src/signature_v4.rs +++ b/src/signature_v4.rs @@ -520,6 +520,26 @@ mod tests { ); } + #[test] + fn canonical_uri_encodes_plus_in_key() { + // A decoded key with '+' must be percent-encoded exactly once into the canonical URI. + // The auth path feeds in the decoded request path, so '+' -> %2B, matching the client; + // feeding the raw wire path would encode a second time ('+' -> %252B). + let headers = OrderedHeaders::from_slice_unchecked(&[("host", "b.s3.amazonaws.com")]); + let qs: &[(String, String)] = &[]; + let canonical_request = create_canonical_request( + &Method::PUT, + "/b/pkg-1.0+post.1.whl", + qs, + &headers, + Payload::Unsigned, + ); + assert_eq!( + canonical_request.lines().nth(1).unwrap(), + "/b/pkg-1.0%2Bwasix.1.whl" + ); + } + #[test] fn example_put_object_single_chunk() { // let access_key_id = "AKIAIOSFODNN7EXAMPLE";