Skip to content

Commit 957695f

Browse files
pietroalbiniJoshua Nelson
authored andcommitted
statics: fix directory traversal protection code and test
1 parent 4379df7 commit 957695f

File tree

1 file changed

+32
-25
lines changed

1 file changed

+32
-25
lines changed

src/web/statics.rs

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,37 @@ pub(crate) fn static_handler(req: &mut Request) -> IronResult<Response> {
2323
"vendored.css" => serve_resource(VENDORED_CSS, ContentType("text/css".parse().unwrap()))?,
2424
"style.css" => serve_resource(STYLE_CSS, ContentType("text/css".parse().unwrap()))?,
2525
"rustdoc.css" => serve_resource(RUSTDOC_CSS, ContentType("text/css".parse().unwrap()))?,
26-
file => serve_file(req, file)?,
26+
file => serve_file(file)?,
2727
})
2828
}
2929

30-
fn serve_file(req: &Request, file: &str) -> IronResult<Response> {
30+
fn serve_file(file: &str) -> IronResult<Response> {
3131
// Find the first path that actually exists
3232
let path = STATIC_SEARCH_PATHS
3333
.iter()
3434
.filter_map(|root| {
3535
let path = Path::new(root).join(file);
36+
if !path.exists() {
37+
return None;
38+
}
3639

3740
// Prevent accessing static files outside the root. This could happen if the path
3841
// contains `/` or `..`. The check doesn't outright prevent those strings to be present
3942
// to allow accessing files in subdirectories.
40-
if path.starts_with(root) {
41-
Some(path)
43+
let canonical_path = std::fs::canonicalize(path).ok()?;
44+
let canonical_root = std::fs::canonicalize(root).ok()?;
45+
if canonical_path.starts_with(canonical_root) {
46+
Some(canonical_path)
4247
} else {
4348
None
4449
}
4550
})
46-
.find(|p| p.exists())
51+
.next()
4752
.ok_or(Nope::ResourceNotFound)?;
48-
let contents = ctry!(req, fs::read(&path));
53+
let contents = fs::read(&path).map_err(|e| {
54+
log::error!("failed to read static file {}: {}", path.display(), e);
55+
Nope::InternalServerError
56+
})?;
4957

5058
// If we can detect the file's mime type, set it
5159
// MimeGuess misses a lot of the file types we need, so there's a small wrapper
@@ -124,7 +132,7 @@ pub(super) fn ico_handler(req: &mut Request) -> IronResult<Response> {
124132

125133
#[cfg(test)]
126134
mod tests {
127-
use super::{STATIC_SEARCH_PATHS, STYLE_CSS, VENDORED_CSS};
135+
use super::{serve_file, STATIC_SEARCH_PATHS, STYLE_CSS, VENDORED_CSS};
128136
use crate::test::wrapper;
129137
use std::fs;
130138

@@ -272,23 +280,22 @@ mod tests {
272280

273281
#[test]
274282
fn directory_traversal() {
275-
wrapper(|env| {
276-
let web = env.frontend();
277-
278-
let urls = &[
279-
"../LICENSE.txt",
280-
"%2e%2e%2fLICENSE.txt",
281-
"%2e%2e/LICENSE.txt",
282-
"..%2fLICENSE.txt",
283-
"%2e%2e%5cLICENSE.txt",
284-
];
285-
286-
for url in urls {
287-
let req = web.get(&format!("/-/static/{}", url)).send()?;
288-
assert_eq!(req.status().as_u16(), 404);
289-
}
290-
291-
Ok(())
292-
});
283+
const PATHS: &[&str] = &[
284+
"../LICENSE",
285+
"%2e%2e%2fLICENSE",
286+
"%2e%2e/LICENSE",
287+
"..%2fLICENSE",
288+
"%2e%2e%5cLICENSE",
289+
];
290+
291+
for path in PATHS {
292+
// This doesn't test an actual web request as the web framework used at the time of
293+
// writing this test (iron 0.5) already resolves `..` before calling any handler.
294+
//
295+
// Still, the test ensures the underlying function called by the request handler to
296+
// serve the file also includes protection for path traversal, in the event we switch
297+
// to a framework that doesn't include builtin protection in the future.
298+
assert!(serve_file(path).is_err(), "{} was served", path);
299+
}
293300
}
294301
}

0 commit comments

Comments
 (0)