11use std:: fs:: File ;
22use std:: io:: { self , Read , Seek } ;
3- use std:: path:: { Path , PathBuf } ;
3+ use std:: path:: { Component , Path , PathBuf } ;
44use std:: time:: SystemTime ;
55
66use fluxheim_config:: { DirectoryListingConfig , WebConfig } ;
@@ -90,6 +90,12 @@ impl NativeHttp1StaticWeb {
9090 }
9191
9292 pub fn handle ( & self , request : & NativeHttp1Request , request_path : & str ) -> NativeHttp1Response {
93+ if !static_web_method_allowed ( & request. method ) {
94+ return NativeHttp1Response :: new ( 405 , "Method Not Allowed" , b"method not allowed\n " )
95+ . with_header ( "allow" , "GET, HEAD" )
96+ . close_connection ( ) ;
97+ }
98+
9399 match self . resolve ( request_path) {
94100 Ok ( NativeStaticResolve :: Found ( file) ) => self . file_response ( request, & file) ,
95101 Ok ( NativeStaticResolve :: DirectoryListing ( listing) ) => {
@@ -409,14 +415,61 @@ fn open_static_body_file(file: &NativeStaticFile) -> io::Result<File> {
409415 "static body path escaped web root" ,
410416 ) ) ;
411417 }
412- let metadata = std:: fs:: symlink_metadata ( & canonical) ?;
413- if metadata. file_type ( ) . is_symlink ( ) || !metadata. is_file ( ) {
418+ let opened = open_static_body_file_at_root ( file, & relative. as_path ( ) ) ?;
419+ let metadata = opened. metadata ( ) ?;
420+ if !metadata. is_file ( ) {
414421 return Err ( io:: Error :: new (
415422 io:: ErrorKind :: InvalidInput ,
416423 "static body path is not a regular file" ,
417424 ) ) ;
418425 }
419- File :: open ( canonical)
426+ Ok ( opened)
427+ }
428+
429+ fn open_static_body_file_at_root (
430+ file : & NativeStaticFile ,
431+ relative_path : & Path ,
432+ ) -> io:: Result < File > {
433+ let directory_flags =
434+ rustix:: fs:: OFlags :: RDONLY | rustix:: fs:: OFlags :: DIRECTORY | rustix:: fs:: OFlags :: CLOEXEC ;
435+ let nofollow_directory_flags = directory_flags | rustix:: fs:: OFlags :: NOFOLLOW ;
436+ let file_flags =
437+ rustix:: fs:: OFlags :: RDONLY | rustix:: fs:: OFlags :: CLOEXEC | rustix:: fs:: OFlags :: NOFOLLOW ;
438+
439+ let mut directory = rustix:: fs:: open (
440+ & file. root ,
441+ nofollow_directory_flags,
442+ rustix:: fs:: Mode :: empty ( ) ,
443+ )
444+ . map_err ( io:: Error :: from) ?;
445+ let mut components = relative_path. components ( ) . peekable ( ) ;
446+ while let Some ( component) = components. next ( ) {
447+ let Component :: Normal ( name) = component else {
448+ return Err ( io:: Error :: new (
449+ io:: ErrorKind :: InvalidInput ,
450+ "static body path is not relative" ,
451+ ) ) ;
452+ } ;
453+ let name = Path :: new ( name) ;
454+ if components. peek ( ) . is_some ( ) {
455+ directory = rustix:: fs:: openat (
456+ & directory,
457+ name,
458+ nofollow_directory_flags,
459+ rustix:: fs:: Mode :: empty ( ) ,
460+ )
461+ . map_err ( io:: Error :: from) ?;
462+ } else {
463+ let file = rustix:: fs:: openat ( & directory, name, file_flags, rustix:: fs:: Mode :: empty ( ) )
464+ . map_err ( io:: Error :: from) ?;
465+ return Ok ( File :: from ( file) ) ;
466+ }
467+ }
468+
469+ Err ( io:: Error :: new (
470+ io:: ErrorKind :: InvalidInput ,
471+ "static body path is empty" ,
472+ ) )
420473}
421474
422475fn directory_listing_response (
@@ -436,6 +489,10 @@ fn directory_listing_response(
436489 . with_header ( "cache-control" , "private, no-store" )
437490}
438491
492+ fn static_web_method_allowed ( method : & str ) -> bool {
493+ matches ! ( method, "GET" | "HEAD" )
494+ }
495+
439496fn static_reason ( status : u16 ) -> & ' static str {
440497 match status {
441498 200 => "OK" ,
@@ -470,3 +527,36 @@ fn content_type_for_path(path: &Path) -> &'static str {
470527 _ => "application/octet-stream" ,
471528 }
472529}
530+
531+ #[ cfg( test) ]
532+ mod tests {
533+ use std:: time:: SystemTime ;
534+
535+ use tempfile:: TempDir ;
536+
537+ use super :: { NativeStaticFile , open_static_body_file} ;
538+
539+ #[ cfg( unix) ]
540+ #[ test]
541+ fn open_static_body_file_rejects_symlink_swapped_after_resolution ( ) {
542+ let root = TempDir :: new ( ) . unwrap ( ) ;
543+ let asset = root. path ( ) . join ( "asset.txt" ) ;
544+ let outside = root. path ( ) . join ( "outside.txt" ) ;
545+ std:: fs:: write ( & asset, b"safe" ) . unwrap ( ) ;
546+ std:: fs:: write ( & outside, b"outside" ) . unwrap ( ) ;
547+ let root_path = root. path ( ) . canonicalize ( ) . unwrap ( ) ;
548+ let file = NativeStaticFile {
549+ root : root_path. clone ( ) ,
550+ path : root_path. join ( "asset.txt" ) ,
551+ mime : "text/plain; charset=utf-8" ,
552+ len : 4 ,
553+ modified : Some ( SystemTime :: UNIX_EPOCH ) ,
554+ } ;
555+
556+ std:: fs:: remove_file ( & asset) . unwrap ( ) ;
557+ std:: os:: unix:: fs:: symlink ( & outside, & asset) . unwrap ( ) ;
558+
559+ assert ! ( open_static_body_file( & file) . is_err( ) ) ;
560+ root. close ( ) . unwrap ( ) ;
561+ }
562+ }
0 commit comments