fix(odm): reject ambiguous native source dot segments (#7263)

* fix(odm): reject ambiguous native source dot segments

* docs(odm): align native provider limitations with implementation
This commit is contained in:
Zhengchao An
2026-09-06 13:11:35 +08:00
committed by GitHub
parent a8aaadb886
commit 5aef1796cc
4 changed files with 74 additions and 3 deletions
+12
View File
@@ -917,6 +917,18 @@ mod tests {
assert!(head.sse.is_none());
}
#[tokio::test]
async fn dot_segment_keys_fail_before_any_source_request() {
let (endpoint, recorded) = scripted_server(Vec::new()).await;
let backend = backend(&endpoint, Credential::SharedKey(vec![7_u8; 32]));
for key in [".", "..", "dir/./key", "dir/../key", "\u{fffe}/../key"] {
assert!(matches!(backend.head(key).await, Err(SourceError::Unsupported(_))), "HEAD {key:?}");
assert!(matches!(backend.get(key, None).await, Err(SourceError::Unsupported(_))), "GET {key:?}");
assert!(matches!(backend.tagging(key).await, Err(SourceError::Unsupported(_))), "tags {key:?}");
}
assert!(recorded.lock().expect("recorder lock").is_empty());
}
#[tokio::test]
async fn sas_credentials_travel_in_the_query_and_never_sign() {
let (endpoint, recorded) = scripted_server(vec![ScriptedResponse::new(200, blob_headers(), String::new())]).await;
+11
View File
@@ -372,6 +372,17 @@ mod tests {
]
}
#[tokio::test]
async fn dot_segment_keys_fail_before_any_source_request() {
let (endpoint, recorded) = scripted_server(Vec::new()).await;
let backend = backend(&endpoint);
for key in [".", "..", "dir/./key", "dir/../key", "\u{fffe}/../key"] {
assert!(matches!(backend.head(key).await, Err(SourceError::Unsupported(_))), "HEAD {key:?}");
assert!(matches!(backend.get(key, None).await, Err(SourceError::Unsupported(_))), "GET {key:?}");
}
assert!(recorded.lock().expect("recorder lock").is_empty());
}
#[test]
fn objects_list_maps_items_prefixes_and_the_page_token() {
let page = parse_objects_list(LIST_PAGE_ONE).expect("page should parse");
+49 -1
View File
@@ -116,7 +116,15 @@ impl NativeHttp {
.path_segments_mut()
.map_err(|_| SourceError::Other("source endpoint cannot carry a path".to_string()))?;
path.clear();
path.extend(segments);
for segment in segments {
// URL normalization drops standalone dot segments. Sending
// that URL could fetch another object and backfill its bytes
// under the originally requested key.
if matches!(segment, "." | "..") {
return Err(SourceError::Unsupported("source path contains an unsupported dot segment".to_string()));
}
path.push(segment);
}
}
Ok(url)
}
@@ -431,4 +439,44 @@ mod tests {
assert_eq!(url.as_str(), "https://acct.blob.core.windows.net/container/dir/a%20b%3Fc%23d.txt");
assert_eq!(url.query(), None, "a key with '?' must not become a query");
}
#[test]
fn native_http_refuses_dot_segments_instead_of_addressing_another_object() {
let http = NativeHttp::for_test(Url::parse("https://source.example.com").expect("origin"));
for key in [
".",
"..",
"./key",
"../key",
"dir/./key",
"dir/../key",
"dir/.",
"dir/..",
"\u{fffe}/../key",
] {
let error = http
.url(std::iter::once("bucket").chain(key.split('/')))
.expect_err("dot segments must not disappear");
assert!(matches!(error, SourceError::Unsupported(_)), "{key:?}: {error}");
}
}
#[test]
fn native_http_preserves_ordinary_dots_empty_segments_and_literal_escapes() {
let http = NativeHttp::for_test(Url::parse("https://source.example.com").expect("origin"));
for (key, path) in [
("file.txt", "/bucket/file.txt"),
(".hidden/.../tail.", "/bucket/.hidden/.../tail."),
("/dir//key/", "/bucket//dir//key/"),
("%2e/%2E%2E/key", "/bucket/%252e/%252E%252E/key"),
("a+b &?#", "/bucket/a+b%20&%3F%23"),
] {
let url = http
.url(std::iter::once("bucket").chain(key.split('/')))
.expect("representable key");
assert_eq!(url.path(), path, "{key:?}");
assert!(url.query().is_none());
assert!(url.fragment().is_none());
}
}
}