修复 Web 静态资源目录穿越

static_handler 此前直接 join 未归一化的 URI 路径,
curl --path-as-is .../../../ 可读取进程权限内任意文件。
改为逐组件校验:仅允许 Normal/CurDir 组件,拒绝 ..、根路径、盘符。
补充路径校验单元测试。
This commit is contained in:
lbl
2026-08-20 22:16:01 +08:00
parent bf2458d464
commit bd79844849
+52 -1
View File
@@ -444,12 +444,28 @@ fn build_headers_for_path(path: &str) -> HeaderMap {
);
headers
}
/// 将请求路径安全地映射到 static 目录内。
/// 逐组件校验,拒绝 `..`、根路径、盘符等任何可能逃逸出 static 的路径。
fn resolve_static_path(path: &str) -> Option<PathBuf> {
let mut local_path = PathBuf::from("static");
for component in Path::new(path).components() {
match component {
std::path::Component::Normal(part) => local_path.push(part),
std::path::Component::CurDir => {}
_ => return None,
}
}
Some(local_path)
}
async fn static_handler(uri: Uri) -> impl IntoResponse {
let path = uri.path().trim_start_matches('/');
let path = if path.is_empty() { "index.html" } else { path };
// 先尝试从本地文件读取
let local_path = Path::new("static").join(path);
let Some(local_path) = resolve_static_path(path) else {
return (StatusCode::NOT_FOUND, "404 Not Found").into_response();
};
if local_path.is_file()
&& let Ok(content) = tokio::fs::read(&local_path).await
{
@@ -1124,3 +1140,38 @@ async fn get_routes(State(state): State<HttpAppState>) -> Json<ApiResponse<Vec<H
Json(ApiResponse::success(items))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_static_path_allows_normal_paths() {
assert_eq!(
resolve_static_path("index.html"),
Some(PathBuf::from("static").join("index.html"))
);
assert_eq!(
resolve_static_path("css/style.css"),
Some(PathBuf::from("static").join("css").join("style.css"))
);
assert_eq!(
resolve_static_path("./index.html"),
Some(PathBuf::from("static").join("index.html"))
);
}
#[test]
fn test_resolve_static_path_rejects_traversal() {
assert!(resolve_static_path("../Cargo.toml").is_none());
assert!(resolve_static_path("a/../../Cargo.toml").is_none());
assert!(resolve_static_path("/etc/passwd").is_none());
assert!(resolve_static_path("..").is_none());
// Windows 下反斜杠也是路径分隔符
#[cfg(windows)]
{
assert!(resolve_static_path("..\\..\\Cargo.toml").is_none());
assert!(resolve_static_path("C:/Windows/win.ini").is_none());
}
}
}