diff --git a/infrastructure/src/official_download.rs b/infrastructure/src/official_download.rs index d19d1da..dce51bf 100644 --- a/infrastructure/src/official_download.rs +++ b/infrastructure/src/official_download.rs @@ -1320,9 +1320,11 @@ impl OfficialResourcePullService { return Err(format!("官方 URL 包含不安全路径片段:{url}")); } - let segment = segment.split(['?', '#']).next().unwrap_or(segment); - if segment.is_empty() { - continue; + // 官方资源 URL 不携带 query/fragment。若出现则直接拒绝,而非静默剥除—— + // 否则仅 query 不同的两个 URL 会映射到同一目标文件而相互覆盖, + // 并导致每轮 hash 复用校验失配、反复重下。 + if segment.contains('?') || segment.contains('#') { + return Err(format!("官方资源 URL 不允许包含 query 或 fragment:{url}")); } relative_destination.push(sanitize_segment(segment)); } @@ -2193,6 +2195,30 @@ exit 22 assert!(error.contains("危险路径")); } + #[test] + fn rejects_resource_url_with_query_or_fragment() { + let temp = TempDir::new().unwrap(); + let service = OfficialResourcePullService::new(temp.path().join("out")); + + for url in [ + "https://prod-clientpatch.bluearchiveyostar.com/r93_token/TableBundles/TableCatalog.bytes?v=1", + "https://prod-clientpatch.bluearchiveyostar.com/r93_token/TableBundles/TableCatalog.bytes#frag", + ] { + let error = service.destination_for_url(url).unwrap_err(); + assert!( + error.contains("query 或 fragment"), + "unexpected error for {url}: {error}" + ); + } + + // 无 query 的正常资源 URL 仍可映射。 + assert!(service + .destination_for_url( + "https://prod-clientpatch.bluearchiveyostar.com/r93_token/TableBundles/TableCatalog.bytes", + ) + .is_ok()); + } + #[cfg(unix)] #[test] fn pull_rejects_symlink_parent_escape() {