spacecookie 1.0.0.2 → 1.0.0.3
raw patch · 7 files changed
+110/−12 lines, 7 filesdep ~aesondep ~asyncdep ~attoparsecPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: aeson, async, attoparsec, bytestring, containers, directory, download-curl, fast-logger, hxt-unicode, mtl, process, socket, systemd, tasty, tasty-expected-failure, tasty-hunit, text, unix
API changes (from Hackage documentation)
Files
- CHANGELOG.md +23/−1
- server/Network/Spacecookie/FileType.hs +1/−0
- spacecookie.cabal +4/−3
- src/Network/Gopher/Util.hs +6/−3
- src/Network/Gopher/Util/Gophermap.hs +9/−5
- test/EntryPoint.hs +3/−0
- test/Test/Sanitization.hs +64/−0
CHANGELOG.md view
@@ -1,5 +1,27 @@ # Revision history for spacecookie +## 1.0.0.3++2025-05-03++**Security fix**:+Resolve `sanitizePath` not eliminating `..` from paths. This affects users+of `sanitizePath` and `sanitizePathIfNotUrl` from `Network.Gopher.Util`.++This issue only affects the spacecookie library, not the spacecookie server+daemon since a separate check would prevent it from handling such malicious+requests (which delayed the discovery of this bug). It is probably wise to+upgrade either way.++Note that gophermap parsing behavior is unchanged, i.e. it just `normalise`s+paths, even though `makeGophermapFilePath` used to call `sanitizePath` in+previous versions. This is due to the assumption that gophermaps come from a+trusted source and/or paths produced from gophermap parsing aren't used to+access files directly, i.e. those paths are only served to clients (whose later+requests are subject to selector sanitization) as selectors in menus. If those+assumptions don't hold for your code, you will need to further sanitize the+paths returned from `gophermapToDirectoryResponse`.+ ## 1.0.0.2 2022-10-03@@ -25,7 +47,7 @@ added, but old configuration stays compatible. However some gophermap files may need adjusting, especially if they contain absolute paths not starting with a slash.-* For library users there are multiple braking changes to the core API+* For library users there are multiple breaking changes to the core API that probably need adjusting in downstream usage as well as some changes to behavior.
server/Network/Spacecookie/FileType.hs view
@@ -4,6 +4,7 @@ , gopherFileType -- exposed for tests , lookupSuffix+ , checkNoDotFiles ) where import Control.Applicative ((<|>))
spacecookie.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: spacecookie-version: 1.0.0.2+version: 1.0.0.3 synopsis: Gopher server library and daemon description: Simple gopher library that allows writing custom gopher applications. Also includes a fully-featured gopher server@@ -94,6 +94,7 @@ other-modules: Test.FileTypeDetection , Test.Gophermap , Test.Integration+ , Test.Sanitization , Network.Spacecookie.FileType build-depends: tasty >=1.2 , tasty-hunit >=0.10@@ -104,8 +105,8 @@ source-repository head type: git- location: git://github.com/sternenseemann/spacecookie.git+ location: https://github.com/sternenseemann/spacecookie.git source-repository head type: git- location: git://code.sterni.lv/spacecookie+ location: https://code.sterni.lv/spacecookie
src/Network/Gopher/Util.hs view
@@ -27,7 +27,7 @@ import Data.Char (ord, chr, toLower) import qualified Data.String.UTF8 as U import Data.Word (Word8 ())-import System.FilePath.Posix.ByteString (RawFilePath, normalise, joinPath, splitPath)+import System.FilePath.Posix.ByteString (RawFilePath, normalise, joinPath, splitPath, equalFilePath) import System.Posix.User -- | 'chr' a 'Word8'@@ -67,8 +67,11 @@ -- | Normalise a path and prevent <https://en.wikipedia.org/wiki/Directory_traversal_attack directory traversal attacks>. sanitizePath :: RawFilePath -> RawFilePath-sanitizePath = joinPath- . filter (\p -> p /= ".." && p /= ".")+sanitizePath =+ -- To retain prior behavior @"."@ after normalisation is mapped to @""@+ (\p -> if p == "." then "" else p)+ . joinPath+ . filter (\p -> not (equalFilePath p "..")) . splitPath . normalise -- | Use 'sanitizePath' except if the path starts with @URL:@
src/Network/Gopher/Util/Gophermap.hs view
@@ -40,7 +40,7 @@ import Data.Maybe (fromMaybe) import qualified Data.String.UTF8 as U import Data.Word (Word8 ())-import System.FilePath.Posix.ByteString (RawFilePath, (</>))+import System.FilePath.Posix.ByteString (RawFilePath, (</>), isAbsolute, normalise) -- | Given a directory and a Gophermap contained within it, -- return the corresponding gopher menu response.@@ -54,6 +54,8 @@ where realPath p = case p of GophermapAbsolute p' -> p'+ -- TODO: `..` should be resolved textually for linking to the+ -- parent directory (if possible) GophermapRelative p' -> dir </> p' GophermapUrl u -> u @@ -77,10 +79,12 @@ -- considered as an external URL and left as-is. -- * everything else is considered a relative path makeGophermapFilePath :: ByteString -> GophermapFilePath-makeGophermapFilePath b =- fromMaybe (GophermapRelative $ sanitizePath b)- $ boolToMaybe ("URL:" `isPrefixOf` b) (GophermapUrl b)- <|> boolToMaybe ("/" `isPrefixOf` b) (GophermapAbsolute $ sanitizePath b)+makeGophermapFilePath b+ | "URL:" `isPrefixOf` b = GophermapUrl b+ | isAbsolute b = GophermapAbsolute normalisedPath+ | otherwise = GophermapRelative normalisedPath+ where+ normalisedPath = normalise b -- | A gophermap entry makes all values of a gopher menu item optional except for file type and description. When converting to a 'GopherMenuItem', appropriate default values are used. data GophermapEntry = GophermapEntry
test/EntryPoint.hs view
@@ -4,6 +4,8 @@ -- library tests import Test.Gophermap+-- library-ish+import Test.Sanitization -- server executable tests import Test.FileTypeDetection@@ -15,6 +17,7 @@ tests :: TestTree tests = testGroup "tests" [ gophermapTests+ , sanitizationTests , fileTypeDetectionTests , integrationTests ]
+ test/Test/Sanitization.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE OverloadedStrings #-}+module Test.Sanitization (sanitizationTests) where++import Network.Gopher.Util (uEncode, sanitizePath)+import Network.Spacecookie.FileType (checkNoDotFiles, PathError (..))++import Control.Monad (forM_)+import Test.Tasty+import Test.Tasty.HUnit++sanitizationTests :: TestTree+sanitizationTests = testGroup "Sanitization of user input"+ [ pathSanitization+ , dotFileDetectionTest+ ]++pathSanitization :: TestTree+pathSanitization = testCase "sanitizePath behavior" $ do+ let assertSanitize e p = assertEqual p e $ sanitizePath (uEncode p)+ assertSanitize "/root" "/root"+ assertSanitize "/home/alice/.emacs.d/init.el" "/home/alice/.emacs.d/init.el"++ assertSanitize "root" "./root"+ assertSanitize"/tools/magrathea" "//tools/magrathea"+ assertSanitize "/home/bob/Documents/important.txt" "/home/bob//Documents/important.txt"++ assertSanitize "foo/bar/baz.txt" "./foo/bar/./baz.txt"+ assertSanitize "/var/www/index..html" "/var/www/.///index..html"+ assertSanitize "./" "./."+ assertSanitize "/" "/."+ assertSanitize "home/eve/" "./home/./././eve////./."++ assertSanitize "/home/bob/alice/private.txt" "/home/bob/../alice/private.txt"++dotFileDetectionTest :: TestTree+dotFileDetectionTest = testCase "spacecookie server detects dot files in paths" $ do+ let assertDot p hasDot = forM_+ [ (p, uEncode p)+ , (p ++ " (sanitized)", sanitizePath (uEncode p))+ ]+ $ \(title, path) -> assertEqual title+ (if hasDot then Left PathIsNotAllowed else Right ())+ $ checkNoDotFiles path++ assertDot "./normal/relative/path" False+ assertDot "." False+ assertDot "/some/absolute/path" False+ assertDot "file.txt" False+ assertDot "/foo.html" False+ assertDot "./tmp/scratch.txt" False++ assertDot ".emacs.d/init.el" True+ assertDot ".gophermap" True+ assertDot "/home/bob/.vimrc" True+ assertDot "/home/alice/.config/foot" True+ assertDot "./nixpkgs/.git/config" True++ -- only fail prior to sanitization+ forM_+ [ "./.", "relative/./path", "dir/../traversal/../attack", "../../../actual/traversal" ]+ $ \p -> do+ let p' = uEncode p+ assertEqual p (Left PathIsNotAllowed) $ checkNoDotFiles p'+ assertEqual p (Right ()) $ checkNoDotFiles (sanitizePath p')