From d51d26b0ceb34203a88d380332e0ca53cc784ed5 Mon Sep 17 00:00:00 2001 From: Zihan Dai <99155080+PDGGK@users.noreply.github.com> Date: Sat, 15 Aug 2026 00:50:26 +1000 Subject: [PATCH] fix(services): percent-encode the pagination marker in list queries QueryPairsWriter::push does no escaping of its own and says so: /// The input key and value must already been percent /// encoded correctly. Five list builders hand it a raw marker. The value is whatever the server returned as the resume point -- an object key for obs, cos and swift, an opaque continuation token for azblob and azfile -- and for swift's first page it is the user's own start_after argument, so no pagination is needed to reach it. Feeding the exact strings to http::Request::get shows what is sent: report 2024.csv BUILD ERROR: invalid uri character a&b=c marker=a plus a stray b=c parameter note#1.txt marker=note -- the rest is taken as a fragment AB+cd== marker=AB+cd==, whose + a server decodes as a space The middle two are the dangerous ones: the marker silently rewinds, the server resends a page that was already delivered, and since done is computed from the marker being empty the listing can repeat that page indefinitely. The repository already does this correctly in three places, one of them in the same file as a site being fixed here: s3/src/core.rs:803 push("marker", &percent_encode_path(marker)) cos/src/core.rs:606 push("key-marker", &percent_encode_path(key_marker)) oss/src/core.rs:478 push("continuation-token", &percent_encode_path(token)) percent_encode_path is the right helper for a marker specifically: it leaves `/` alone, which an object key needs. That is the difference from #7888, which is encoding cos and tos versionId values in the same family of defect and introduces a stricter set for them -- a version id is not a path, a marker is. No new tests. Each change is one call to a helper the file already imports, and asserting on it would mean restructuring five URL builders, which I would rather not fold into a fix. All 39 existing unit tests across the five crates pass, and none of them assert on a marker URL. --- core/services/azblob/src/core.rs | 2 +- core/services/azfile/src/core.rs | 2 +- core/services/cos/src/core.rs | 2 +- core/services/obs/src/core.rs | 2 +- core/services/swift/src/core.rs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/services/azblob/src/core.rs b/core/services/azblob/src/core.rs index 1bb10e08dca7..c70ad3a36226 100644 --- a/core/services/azblob/src/core.rs +++ b/core/services/azblob/src/core.rs @@ -801,7 +801,7 @@ impl AzblobCore { url = url.push("delimiter", delimiter); } if !next_marker.is_empty() { - url = url.push("marker", next_marker); + url = url.push("marker", &percent_encode_path(next_marker)); } let req = Request::get(url.finish()) diff --git a/core/services/azfile/src/core.rs b/core/services/azfile/src/core.rs index fc1395d70895..c0e7c3991cac 100644 --- a/core/services/azfile/src/core.rs +++ b/core/services/azfile/src/core.rs @@ -433,7 +433,7 @@ impl AzfileCore { .push("include", "Timestamps,ETag"); if !continuation.is_empty() { - url = url.push("marker", continuation); + url = url.push("marker", &percent_encode_path(continuation)); } if let Some(limit) = limit { diff --git a/core/services/cos/src/core.rs b/core/services/cos/src/core.rs index ec07da45bf3a..55e319040ad9 100644 --- a/core/services/cos/src/core.rs +++ b/core/services/cos/src/core.rs @@ -410,7 +410,7 @@ impl CosCore { url = url.push("max-keys", &limit.to_string()); } if !next_marker.is_empty() { - url = url.push("marker", next_marker); + url = url.push("marker", &percent_encode_path(next_marker)); } let req = Request::get(url.finish()) diff --git a/core/services/obs/src/core.rs b/core/services/obs/src/core.rs index f2333c20144b..e800511d991e 100644 --- a/core/services/obs/src/core.rs +++ b/core/services/obs/src/core.rs @@ -351,7 +351,7 @@ impl ObsCore { url = url.push("max-keys", &limit.to_string()); } if !next_marker.is_empty() { - url = url.push("marker", next_marker); + url = url.push("marker", &percent_encode_path(next_marker)); } let req = Request::get(url.finish()) diff --git a/core/services/swift/src/core.rs b/core/services/swift/src/core.rs index 30399a42f7b7..486997f2b733 100644 --- a/core/services/swift/src/core.rs +++ b/core/services/swift/src/core.rs @@ -214,7 +214,7 @@ impl SwiftCore { url = url.push("limit", &limit.to_string()); } if !marker.is_empty() { - url = url.push("marker", marker); + url = url.push("marker", &percent_encode_path(marker)); } let mut req = Request::get(url.finish());