@@ -60,16 +60,18 @@ import Control.Monad.Trans (MonadIO(liftIO))
6060import qualified Data.ByteString.Lazy.Char8 as L
6161import qualified Data.ByteString.Char8 as S
6262import Data.Data (Data , Typeable )
63- import Data.List (sort )
63+ import Data.Foldable (toList )
64+ import Data.List (sort , isPrefixOf )
6465import Data.Maybe (fromMaybe )
6566import Data.Map (Map )
6667import qualified Data.Map as Map
67- import Filesystem.Path.CurrentOS (commonPrefix , encodeString , decodeString , collapse , append )
68+ import Data.Sequence (Seq ((:|>) , (:<|) ), (|>) , (<|) )
69+ import qualified Data.Sequence as Seq
6870import Happstack.Server.Monads (ServerMonad (askRq ), FilterMonad , WebMonad )
6971import Happstack.Server.Response (ToMessage (toResponse ), ifModifiedSince , forbidden , ok , seeOther )
7072import Happstack.Server.Types (Length (ContentLength ), Request (rqPaths , rqUri ), Response (SendFile ), RsFlags (rsfLength ), nullRsFlags , result , resultBS , setHeader )
71- import System.Directory (doesDirectoryExist , doesFileExist , getDirectoryContents , getModificationTime )
72- import System.FilePath ((</>) , addTrailingPathSeparator , hasDrive , isPathSeparator , joinPath , takeExtension , isValid )
73+ import System.Directory (doesDirectoryExist , doesFileExist , getDirectoryContents , getModificationTime , makeAbsolute )
74+ import System.FilePath ((</>) , addTrailingPathSeparator , hasDrive , isPathSeparator , joinPath , takeExtension , isValid , normalise , splitPath , isAbsolute )
7375import System.IO (IOMode (ReadMode ), hFileSize , hClose , openBinaryFile , withBinaryFile )
7476import System.Log.Logger (Priority (DEBUG ), logM )
7577import Text.Blaze.Html ((!) )
@@ -319,7 +321,7 @@ serveFileFrom :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m)
319321 -> FilePath -- ^ path to the file to serve
320322 -> m Response
321323serveFileFrom root mimeFn fp =
322- maybe no yes $ combineSafe root fp
324+ combineSafe root fp >>= maybe no yes
323325 where
324326 no = forbidden $ toResponse " Directory traversal forbidden"
325327 yes = serveFile mimeFn
@@ -383,15 +385,42 @@ fileServe' serveFn mimeFn indexFn localPath = do
383385-- Nothing
384386-- >>> combineSafe "/var/uploads/" "../uploads/home/../etc/passwd"
385387-- Just "/var/uploads/etc/passwd"
386- combineSafe :: FilePath -> FilePath -> Maybe FilePath
387- combineSafe root path =
388- if commonPrefix [root', joined] == root'
389- then Just $ encodeString joined
390- else Nothing
388+ combineSafe :: MonadIO m => FilePath -> FilePath -> m (Maybe FilePath )
389+ combineSafe root path = do
390+ root' <- liftIO $ makeAbsolute root
391+ let path' = normalise path
392+ pure $ do
393+ combined <- combineReduce root' path'
394+ if root' `isPrefixOf` combined
395+ then Just combined
396+ else Nothing
391397 where
392- root' = decodeString root
393- path' = decodeString path
394- joined = collapse $ append root' path'
398+ -- Combine an absolute path with another path, reducing any @..@ elements
399+ combineReduce :: FilePath -> FilePath -> Maybe FilePath
400+ combineReduce r p
401+ | isAbsolute r = Just $
402+ let splitP = splitPath p
403+ in joinPath $ toList $
404+ -- If @p@ is absolute, then process it against the root path, dropping @r@ completely
405+ if isAbsolute p
406+ -- Split off the head and re-add it after processing the tail with @go@
407+ then let headP :<| tailP = Seq. fromList splitP
408+ in headP <| go Seq. Empty (toList tailP)
409+ else let headR :<| tailR = Seq. fromList (splitPath r)
410+ in headR <| go tailR splitP
411+ -- If the root is not absolute, it is unclear how to handle arbitrary @..@ elements in a safe way
412+ | otherwise = Nothing
413+
414+ -- | Build up a 'Seq' representation of @path@, reducing any @..@ elements
415+ -- This function assumes the 'Seq' is a split absolute path, with the beginning part removed.
416+ --
417+ -- Note that this functionality has been removed from the filepath package
418+ -- See: <https://neilmitchell.blogspot.com/2015/10/filepaths-are-subtle-symlinks-are-hard.html>
419+ go :: Seq FilePath -> [FilePath ] -> Seq FilePath
420+ go p [] = p
421+ go Seq. Empty (" .." : _ ) = Seq. Empty -- Going up beyond the top level does nothing
422+ go (s :|> _) (" .." : ps) = go s ps -- Going up a level pops an element off the right side of the Seq
423+ go s (p : ps) = go (s |> p) ps -- Just add an element to the right side of the Seq
395424
396425isSafePath :: [FilePath ] -> Bool
397426isSafePath [] = True
0 commit comments