@@ -23,29 +23,37 @@ pub(crate) fn static_handler(req: &mut Request) -> IronResult<Response> {
23
23
"vendored.css" => serve_resource ( VENDORED_CSS , ContentType ( "text/css" . parse ( ) . unwrap ( ) ) ) ?,
24
24
"style.css" => serve_resource ( STYLE_CSS , ContentType ( "text/css" . parse ( ) . unwrap ( ) ) ) ?,
25
25
"rustdoc.css" => serve_resource ( RUSTDOC_CSS , ContentType ( "text/css" . parse ( ) . unwrap ( ) ) ) ?,
26
- file => serve_file ( req , file) ?,
26
+ file => serve_file ( file) ?,
27
27
} )
28
28
}
29
29
30
- fn serve_file ( req : & Request , file : & str ) -> IronResult < Response > {
30
+ fn serve_file ( file : & str ) -> IronResult < Response > {
31
31
// Find the first path that actually exists
32
32
let path = STATIC_SEARCH_PATHS
33
33
. iter ( )
34
34
. filter_map ( |root| {
35
35
let path = Path :: new ( root) . join ( file) ;
36
+ if !path. exists ( ) {
37
+ return None ;
38
+ }
36
39
37
40
// Prevent accessing static files outside the root. This could happen if the path
38
41
// contains `/` or `..`. The check doesn't outright prevent those strings to be present
39
42
// 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)
42
47
} else {
43
48
None
44
49
}
45
50
} )
46
- . find ( |p| p . exists ( ) )
51
+ . next ( )
47
52
. 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
+ } ) ?;
49
57
50
58
// If we can detect the file's mime type, set it
51
59
// 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> {
124
132
125
133
#[ cfg( test) ]
126
134
mod tests {
127
- use super :: { STATIC_SEARCH_PATHS , STYLE_CSS , VENDORED_CSS } ;
135
+ use super :: { serve_file , STATIC_SEARCH_PATHS , STYLE_CSS , VENDORED_CSS } ;
128
136
use crate :: test:: wrapper;
129
137
use std:: fs;
130
138
@@ -272,23 +280,22 @@ mod tests {
272
280
273
281
#[ test]
274
282
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
+ }
293
300
}
294
301
}
0 commit comments