Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove dependency on deprecated system-filepath #70

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ dist
.env
.anvil
.ghc.environment.*
cabal.project.local
hie.yaml
1 change: 0 additions & 1 deletion happstack-server.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ Library
process,
semigroups >= 0.16,
sendfile >= 0.7.1 && < 0.8,
system-filepath >= 0.3.1,
syb,
text >= 0.10 && < 1.3,
time,
Expand Down
56 changes: 43 additions & 13 deletions src/Happstack/Server/FileServe/BuildingBlocks.hs
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,18 @@ import Control.Monad.Trans (MonadIO(liftIO))
import qualified Data.ByteString.Lazy.Char8 as L
import qualified Data.ByteString.Char8 as S
import Data.Data (Data, Typeable)
import Data.List (sort)
import Data.Foldable (toList, foldl')
import Data.List (sort, isPrefixOf)
import Data.Maybe (fromMaybe)
import Data.Map (Map)
import qualified Data.Map as Map
import Filesystem.Path.CurrentOS (commonPrefix, encodeString, decodeString, collapse, append)
import Data.Sequence (Seq ((:|>), (:<|)), (|>), (<|))
import qualified Data.Sequence as Seq
import Happstack.Server.Monads (ServerMonad(askRq), FilterMonad, WebMonad)
import Happstack.Server.Response (ToMessage(toResponse), ifModifiedSince, forbidden, ok, seeOther)
import Happstack.Server.Types (Length(ContentLength), Request(rqPaths, rqUri), Response(SendFile), RsFlags(rsfLength), nullRsFlags, result, resultBS, setHeader)
import System.Directory (doesDirectoryExist, doesFileExist, getDirectoryContents, getModificationTime)
import System.FilePath ((</>), addTrailingPathSeparator, hasDrive, isPathSeparator, joinPath, takeExtension, isValid)
import System.Directory (doesDirectoryExist, doesFileExist, getDirectoryContents, getModificationTime, makeAbsolute)
import System.FilePath ((</>), addTrailingPathSeparator, hasDrive, isPathSeparator, joinPath, takeExtension, isValid, normalise, splitDirectories, isAbsolute)
import System.IO (IOMode(ReadMode), hFileSize, hClose, openBinaryFile, withBinaryFile)
import System.Log.Logger (Priority(DEBUG), logM)
import Text.Blaze.Html ((!))
Expand Down Expand Up @@ -319,7 +321,7 @@ serveFileFrom :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m)
-> FilePath -- ^ path to the file to serve
-> m Response
serveFileFrom root mimeFn fp =
maybe no yes $ combineSafe root fp
combineSafe root fp >>= maybe no yes
where
no = forbidden $ toResponse "Directory traversal forbidden"
yes = serveFile mimeFn
Expand Down Expand Up @@ -383,15 +385,43 @@ fileServe' serveFn mimeFn indexFn localPath = do
-- Nothing
-- >>> combineSafe "/var/uploads/" "../uploads/home/../etc/passwd"
-- Just "/var/uploads/etc/passwd"
combineSafe :: FilePath -> FilePath -> Maybe FilePath
combineSafe root path =
if commonPrefix [root', joined] == root'
then Just $ encodeString joined
else Nothing
combineSafe :: MonadIO m => FilePath -> FilePath -> m (Maybe FilePath)
combineSafe root path = do
root' <- liftIO $ makeAbsolute root
let path' = normalise path
pure $
case combineReduce root' path' of
Just combined | root' `isPrefixOf` combined ->
Just combined
_ ->
Nothing
where
root' = decodeString root
path' = decodeString path
joined = collapse $ append root' path'
-- Combine an absolute path with another path, reducing any @..@ elements
combineReduce :: FilePath -> FilePath -> Maybe FilePath
combineReduce r p
| isAbsolute r = Just $
let splitP = splitDirectories p
splitR = splitDirectories r
-- Split off the head and re-add it after processing the tail with @go@
headP :<| tailP = Seq.fromList splitP
headR :<| tailR = Seq.fromList splitR
in joinPath $ toList $
-- If @p@ is absolute, then process it against the root path, dropping @r@ completely
if isAbsolute p
then headP <| foldl' go Seq.Empty (toList tailP)
else headR <| foldl' go tailR splitP
-- If the root is not absolute, it is unclear how to handle arbitrary @..@ elements in a safe way
| otherwise = Nothing

-- | Build up a 'Seq' representation of @path@, reducing any @..@ elements
-- This function assumes the 'Seq' is a split absolute path, with the beginning part removed.
--
-- Note that this functionality has been removed from the filepath package
-- See: <https://neilmitchell.blogspot.com/2015/10/filepaths-are-subtle-symlinks-are-hard.html>
go :: Seq FilePath -> FilePath -> Seq FilePath
go Seq.Empty ".." = Seq.Empty -- Going up beyond the top level just returns the top level
go (s :|> _) ".." = s -- Going up a level pops an element off the right side of the Seq
go s p = s |> p -- Just add an element to the right side of the Seq

isSafePath :: [FilePath] -> Bool
isSafePath [] = True
Expand Down
18 changes: 17 additions & 1 deletion tests/Happstack/Server/Tests.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import qualified Data.ByteString.Lazy as L
import qualified Data.Map as Map
import Happstack.Server ( Request(..), Method(..), Response(..), ServerPart, Headers, RqBody(Body), HttpVersion(..)
, ToMessage(..), HeaderPair(..), ok, dir, simpleHTTP'', composeFilter, noContentLength, matchMethod)
import Happstack.Server.FileServe.BuildingBlocks (sendFileResponse)
import Happstack.Server.FileServe.BuildingBlocks (sendFileResponse, combineSafe)
import Happstack.Server.Cookie
import Happstack.Server.Internal.Compression
import Happstack.Server.Internal.Cookie
Expand All @@ -34,6 +34,7 @@ allTests =
, matchMethodTest
, cookieHeaderOrderTest
, pContentDispositionFilename
, combineSafeTest
]

cookieParserTest :: Test
Expand Down Expand Up @@ -247,3 +248,18 @@ pContentDispositionFilename =
do let doesNotWorkWithOldParserButWithNew = "form-data; filename=\"file.pdf\"; name=\"file\"" :: String
c <- parseContentDisposition doesNotWorkWithOldParserButWithNew
assertEqual "parseContentDisposition" c (ContentDisposition "form-data" [("filename","file.pdf"),("name","file")])

-- | Make sure 'combineSafe' works correctly
combineSafeTest :: Test
combineSafeTest =
"combineSafeTest" ~:
do r1 <- combineSafe "/var/uploads/" "etc/passwd"
r2 <- combineSafe "/var/uploads/" "/etc/passwd"
r3 <- combineSafe "/var/uploads/" "../../etc/passwd"
r4 <- combineSafe "/var/uploads/" "../uploads/home/../etc/passwd"
r5 <- combineSafe "/var/uploads/" "../../../../var/uploads/etc"
r1 @?= Just "/var/uploads/etc/passwd"
r2 @?= Nothing
r3 @?= Nothing
r4 @?= Just "/var/uploads/etc/passwd"
r5 @?= Just "/var/uploads/etc"