Skip to content

Commit f12cadc

Browse files
committed
Use sanitized path in dir redirect
1 parent 2ff8d89 commit f12cadc

File tree

3 files changed

+42
-8
lines changed

3 files changed

+42
-8
lines changed

src/resolve.rs

+16-2
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ pub enum ResolveResult<F = File> {
7878
/// The requested file could not be accessed.
7979
PermissionDenied,
8080
/// A directory was requested as a file.
81-
IsDirectory,
81+
IsDirectory {
82+
/// Path to redirect to.
83+
redirect_to: String,
84+
},
8285
/// The requested file was found.
8386
Found(ResolvedFile<F>),
8487
}
@@ -169,7 +172,18 @@ impl<O: FileOpener> Resolver<O> {
169172

170173
// We may have opened a directory for a file request, in which case we redirect.
171174
if !is_dir_request && file.is_dir {
172-
return Ok(ResolveResult::IsDirectory);
175+
// Build the redirect path. On Windows, we can't just append the entire path, because
176+
// it contains Windows path separators. Instead, append each component separately.
177+
let mut target = String::with_capacity(path.as_os_str().len() + 2);
178+
target.push('/');
179+
for component in path.components() {
180+
target.push_str(&component.as_os_str().to_string_lossy());
181+
target.push('/');
182+
}
183+
184+
return Ok(ResolveResult::IsDirectory {
185+
redirect_to: target,
186+
});
173187
}
174188

175189
// If not a directory, serve this file.

src/response_builder.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,10 @@ impl<'a> ResponseBuilder<'a> {
9191
ResolveResult::PermissionDenied => HttpResponseBuilder::new()
9292
.status(StatusCode::FORBIDDEN)
9393
.body(Body::Empty),
94-
ResolveResult::IsDirectory => {
95-
let mut target = self.path.to_owned();
96-
target.push('/');
94+
ResolveResult::IsDirectory {
95+
redirect_to: mut target,
96+
} => {
97+
// Preserve any query string from the original request.
9798
if let Some(query) = self.query {
9899
target.push('?');
99100
target.push_str(query);

tests/static.rs

+22-3
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,32 @@ async fn returns_404_if_file_not_found() {
118118

119119
#[tokio::test]
120120
async fn redirects_if_trailing_slash_is_missing() {
121-
let harness = Harness::new(vec![("dir/index.html", "this is index")]);
121+
let harness = Harness::new(vec![("foo/bar/index.html", "this is index")]);
122122

123-
let res = harness.get("/dir").await.unwrap();
123+
let res = harness.get("/foo/bar").await.unwrap();
124124
assert_eq!(res.status(), StatusCode::MOVED_PERMANENTLY);
125125

126126
let url = res.headers().get(header::LOCATION).unwrap();
127-
assert_eq!(url, "/dir/");
127+
assert_eq!(url, "/foo/bar/");
128+
}
129+
130+
#[tokio::test]
131+
async fn redirects_to_sanitized_path() {
132+
let harness = Harness::new(vec![("foo.org/bar/index.html", "this is index")]);
133+
134+
// Previous versions would base the redirect on the request path, but that is user input, and
135+
// the user could construct a schema-relative redirect this way.
136+
let res = harness.get("//foo.org/bar").await.unwrap();
137+
assert_eq!(res.status(), StatusCode::MOVED_PERMANENTLY);
138+
139+
let url = res.headers().get(header::LOCATION).unwrap();
140+
// TODO: The request path is apparently parsed differently on Windows, but at least the
141+
// resulting redirect is still safe, and that's the important part.
142+
if cfg!(target_os = "windows") {
143+
assert_eq!(url, "/");
144+
} else {
145+
assert_eq!(url, "/foo.org/bar/");
146+
}
128147
}
129148

130149
#[tokio::test]

0 commit comments

Comments
 (0)