Skip to content

ServeDir/File: Fix build_and_validate_path to prevent hacker from accessing arbitrary files #204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Empty file added test-files/你好世界.txt
Empty file.
39 changes: 19 additions & 20 deletions tower-http/src/services/fs/serve_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use percent_encoding::percent_decode;
use std::fs::Metadata;
use std::io::SeekFrom;
use std::ops::RangeInclusive;
use std::path::Component;
use std::{
future::Future,
io,
Expand Down Expand Up @@ -130,17 +131,28 @@ impl ServeVariant {
}

fn build_and_validate_path(base_path: &Path, requested_path: &str) -> Option<PathBuf> {
// build and validate the path
let path = requested_path.trim_start_matches('/');

let path_decoded = percent_decode(path.as_ref()).decode_utf8().ok()?;
let path_decoded = Path::new(&*path_decoded);

let mut full_path = base_path.to_path_buf();
for seg in path_decoded.split('/') {
if seg.starts_with("..") || seg.contains('\\') {
return None;
for component in path_decoded.components() {
match component {
Component::Normal(comp) => {
// protect against paths like `/foo/c:/bar/baz` (#204)
if Path::new(&comp)
.components()
.all(|c| matches!(c, Component::Normal(_)))
{
full_path.push(comp)
} else {
return None;
}
}
Component::CurDir => {}
Component::Prefix(_) | Component::RootDir | Component::ParentDir => return None,
}
full_path.push(seg);
}
Some(full_path)
}
Expand Down Expand Up @@ -984,15 +996,10 @@ mod tests {

#[tokio::test]
async fn access_cjk_percent_encoded_uri_path() {
let cjk_filename = "你好世界.txt";
// percent encoding present of 你好世界.txt
let cjk_filename_encoded = "%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C.txt";

let tmp_dir = std::env::temp_dir();
let tmp_filename = std::path::Path::new(tmp_dir.as_path()).join(cjk_filename);
let _ = tokio::fs::File::create(&tmp_filename).await.unwrap();

let svc = ServeDir::new(&tmp_dir);
let svc = ServeDir::new("../test-files");

let req = Request::builder()
.uri(format!("/{}", cjk_filename_encoded))
Expand All @@ -1002,20 +1009,13 @@ mod tests {

assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers()["content-type"], "text/plain");
let _ = tokio::fs::remove_file(&tmp_filename).await.unwrap();
}

#[tokio::test]
async fn access_space_percent_encoded_uri_path() {
let raw_filename = "filename with space.txt";
// percent encoding present of "filename with space.txt"
let encoded_filename = "filename%20with%20space.txt";

let tmp_dir = std::env::temp_dir();
let tmp_filename = std::path::Path::new(tmp_dir.as_path()).join(raw_filename);
let _ = tokio::fs::File::create(&tmp_filename).await.unwrap();

let svc = ServeDir::new(&tmp_dir);
let svc = ServeDir::new("../test-files");

let req = Request::builder()
.uri(format!("/{}", encoded_filename))
Expand All @@ -1025,7 +1025,6 @@ mod tests {

assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers()["content-type"], "text/plain");
let _ = tokio::fs::remove_file(&tmp_filename).await.unwrap();
}

#[tokio::test]
Expand Down