|
Packit |
8cecbd |
{-# LANGUAGE DeriveDataTypeable #-}
|
|
Packit |
8cecbd |
-----------------------------------------------------------------------------
|
|
Packit |
8cecbd |
-- |
|
|
Packit |
8cecbd |
-- Module : Codec.Archive.Tar
|
|
Packit |
8cecbd |
-- Copyright : (c) 2008-2012 Duncan Coutts
|
|
Packit |
8cecbd |
-- 2011 Max Bolingbroke
|
|
Packit |
8cecbd |
-- License : BSD3
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
-- Maintainer : duncan@community.haskell.org
|
|
Packit |
8cecbd |
-- Portability : portable
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
-- Perform various checks on tar file entries.
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
-----------------------------------------------------------------------------
|
|
Packit |
8cecbd |
module Codec.Archive.Tar.Check (
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
-- * Security
|
|
Packit |
8cecbd |
checkSecurity,
|
|
Packit |
8cecbd |
FileNameError(..),
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
-- * Tarbombs
|
|
Packit |
8cecbd |
checkTarbomb,
|
|
Packit |
8cecbd |
TarBombError(..),
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
-- * Portability
|
|
Packit |
8cecbd |
checkPortability,
|
|
Packit |
8cecbd |
PortabilityError(..),
|
|
Packit |
8cecbd |
PortabilityPlatform,
|
|
Packit |
8cecbd |
) where
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
import Codec.Archive.Tar.Types
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
import Data.Typeable (Typeable)
|
|
Packit |
8cecbd |
import Control.Exception (Exception)
|
|
Packit |
8cecbd |
import Control.Monad (MonadPlus(mplus))
|
|
Packit |
8cecbd |
import qualified System.FilePath as FilePath.Native
|
|
Packit |
8cecbd |
( splitDirectories, isAbsolute, isValid )
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
import qualified System.FilePath.Windows as FilePath.Windows
|
|
Packit |
8cecbd |
import qualified System.FilePath.Posix as FilePath.Posix
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
--------------------------
|
|
Packit |
8cecbd |
-- Security
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
-- | This function checks a sequence of tar entries for file name security
|
|
Packit |
8cecbd |
-- problems. It checks that:
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
-- * file paths are not absolute
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
-- * file paths do not contain any path components that are \"@..@\"
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
-- * file names are valid
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
-- These checks are from the perspective of the current OS. That means we check
|
|
Packit |
8cecbd |
-- for \"@C:\blah@\" files on Windows and \"\/blah\" files on Unix. For archive
|
|
Packit |
8cecbd |
-- entry types 'HardLink' and 'SymbolicLink' the same checks are done for the
|
|
Packit |
8cecbd |
-- link target. A failure in any entry terminates the sequence of entries with
|
|
Packit |
8cecbd |
-- an error.
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
checkSecurity :: Entries e -> Entries (Either e FileNameError)
|
|
Packit |
8cecbd |
checkSecurity = checkEntries checkEntrySecurity
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
checkEntrySecurity :: Entry -> Maybe FileNameError
|
|
Packit |
8cecbd |
checkEntrySecurity entry = case entryContent entry of
|
|
Packit |
8cecbd |
HardLink link -> check (entryPath entry)
|
|
Packit |
8cecbd |
`mplus` check (fromLinkTarget link)
|
|
Packit |
8cecbd |
SymbolicLink link -> check (entryPath entry)
|
|
Packit |
8cecbd |
`mplus` check (fromLinkTarget link)
|
|
Packit |
8cecbd |
_ -> check (entryPath entry)
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
where
|
|
Packit |
8cecbd |
check name
|
|
Packit |
8cecbd |
| FilePath.Native.isAbsolute name
|
|
Packit |
8cecbd |
= Just $ AbsoluteFileName name
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
| not (FilePath.Native.isValid name)
|
|
Packit |
8cecbd |
= Just $ InvalidFileName name
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
| any (=="..") (FilePath.Native.splitDirectories name)
|
|
Packit |
8cecbd |
= Just $ InvalidFileName name
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
| otherwise = Nothing
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
-- | Errors arising from tar file names being in some way invalid or dangerous
|
|
Packit |
8cecbd |
data FileNameError
|
|
Packit |
8cecbd |
= InvalidFileName FilePath
|
|
Packit |
8cecbd |
| AbsoluteFileName FilePath
|
|
Packit |
8cecbd |
deriving (Typeable)
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
instance Show FileNameError where
|
|
Packit |
8cecbd |
show = showFileNameError Nothing
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
instance Exception FileNameError
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
showFileNameError :: Maybe PortabilityPlatform -> FileNameError -> String
|
|
Packit |
8cecbd |
showFileNameError mb_plat err = case err of
|
|
Packit |
8cecbd |
InvalidFileName path -> "Invalid" ++ plat ++ " file name in tar archive: " ++ show path
|
|
Packit |
8cecbd |
AbsoluteFileName path -> "Absolute" ++ plat ++ " file name in tar archive: " ++ show path
|
|
Packit |
8cecbd |
where plat = maybe "" (' ':) mb_plat
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
--------------------------
|
|
Packit |
8cecbd |
-- Tarbombs
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
-- | This function checks a sequence of tar entries for being a \"tar bomb\".
|
|
Packit |
8cecbd |
-- This means that the tar file does not follow the standard convention that
|
|
Packit |
8cecbd |
-- all entries are within a single subdirectory, e.g. a file \"foo.tar\" would
|
|
Packit |
8cecbd |
-- usually have all entries within the \"foo/\" subdirectory.
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
-- Given the expected subdirectory, this function checks all entries are within
|
|
Packit |
8cecbd |
-- that subdirectroy.
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
-- Note: This check must be used in conjunction with 'checkSecurity'
|
|
Packit |
8cecbd |
-- (or 'checkPortability').
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
checkTarbomb :: FilePath -> Entries e -> Entries (Either e TarBombError)
|
|
Packit |
8cecbd |
checkTarbomb expectedTopDir = checkEntries (checkEntryTarbomb expectedTopDir)
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
checkEntryTarbomb :: FilePath -> Entry -> Maybe TarBombError
|
|
Packit |
8cecbd |
checkEntryTarbomb _ entry | nonFilesystemEntry = Nothing
|
|
Packit |
8cecbd |
where
|
|
Packit |
8cecbd |
-- Ignore some special entries we will not unpack anyway
|
|
Packit |
8cecbd |
nonFilesystemEntry =
|
|
Packit |
8cecbd |
case entryContent entry of
|
|
Packit |
8cecbd |
OtherEntryType 'g' _ _ -> True --PAX global header
|
|
Packit |
8cecbd |
OtherEntryType 'x' _ _ -> True --PAX individual header
|
|
Packit |
8cecbd |
_ -> False
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
checkEntryTarbomb expectedTopDir entry =
|
|
Packit |
8cecbd |
case FilePath.Native.splitDirectories (entryPath entry) of
|
|
Packit |
8cecbd |
(topDir:_) | topDir == expectedTopDir -> Nothing
|
|
Packit |
8cecbd |
_ -> Just $ TarBombError expectedTopDir
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
-- | An error that occurs if a tar file is a \"tar bomb\" that would extract
|
|
Packit |
8cecbd |
-- files outside of the intended directory.
|
|
Packit |
8cecbd |
data TarBombError = TarBombError FilePath
|
|
Packit |
8cecbd |
deriving (Typeable)
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
instance Exception TarBombError
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
instance Show TarBombError where
|
|
Packit |
8cecbd |
show (TarBombError expectedTopDir)
|
|
Packit |
8cecbd |
= "File in tar archive is not in the expected directory " ++ show expectedTopDir
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
--------------------------
|
|
Packit |
8cecbd |
-- Portability
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
-- | This function checks a sequence of tar entries for a number of portability
|
|
Packit |
8cecbd |
-- issues. It will complain if:
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
-- * The old \"Unix V7\" or \"gnu\" formats are used. For maximum portability
|
|
Packit |
8cecbd |
-- only the POSIX standard \"ustar\" format should be used.
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
-- * A non-portable entry type is used. Only ordinary files, hard links,
|
|
Packit |
8cecbd |
-- symlinks and directories are portable. Device files, pipes and others are
|
|
Packit |
8cecbd |
-- not portable between all common operating systems.
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
-- * Non-ASCII characters are used in file names. There is no agreed portable
|
|
Packit |
8cecbd |
-- convention for Unicode or other extended character sets in file names in
|
|
Packit |
8cecbd |
-- tar archives.
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
-- * File names that would not be portable to both Unix and Windows. This check
|
|
Packit |
8cecbd |
-- includes characters that are valid in both systems and the \'/\' vs \'\\\'
|
|
Packit |
8cecbd |
-- directory separator conventions.
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
checkPortability :: Entries e -> Entries (Either e PortabilityError)
|
|
Packit |
8cecbd |
checkPortability = checkEntries checkEntryPortability
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
checkEntryPortability :: Entry -> Maybe PortabilityError
|
|
Packit |
8cecbd |
checkEntryPortability entry
|
|
Packit |
8cecbd |
| entryFormat entry `elem` [V7Format, GnuFormat]
|
|
Packit |
8cecbd |
= Just $ NonPortableFormat (entryFormat entry)
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
| not (portableFileType (entryContent entry))
|
|
Packit |
8cecbd |
= Just NonPortableFileType
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
| not (all portableChar posixPath)
|
|
Packit |
8cecbd |
= Just $ NonPortableEntryNameChar posixPath
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
| not (FilePath.Posix.isValid posixPath)
|
|
Packit |
8cecbd |
= Just $ NonPortableFileName "unix" (InvalidFileName posixPath)
|
|
Packit |
8cecbd |
| not (FilePath.Windows.isValid windowsPath)
|
|
Packit |
8cecbd |
= Just $ NonPortableFileName "windows" (InvalidFileName windowsPath)
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
| FilePath.Posix.isAbsolute posixPath
|
|
Packit |
8cecbd |
= Just $ NonPortableFileName "unix" (AbsoluteFileName posixPath)
|
|
Packit |
8cecbd |
| FilePath.Windows.isAbsolute windowsPath
|
|
Packit |
8cecbd |
= Just $ NonPortableFileName "windows" (AbsoluteFileName windowsPath)
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
| any (=="..") (FilePath.Posix.splitDirectories posixPath)
|
|
Packit |
8cecbd |
= Just $ NonPortableFileName "unix" (InvalidFileName posixPath)
|
|
Packit |
8cecbd |
| any (=="..") (FilePath.Windows.splitDirectories windowsPath)
|
|
Packit |
8cecbd |
= Just $ NonPortableFileName "windows" (InvalidFileName windowsPath)
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
| otherwise = Nothing
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
where
|
|
Packit |
8cecbd |
tarPath = entryTarPath entry
|
|
Packit |
8cecbd |
posixPath = fromTarPathToPosixPath tarPath
|
|
Packit |
8cecbd |
windowsPath = fromTarPathToWindowsPath tarPath
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
portableFileType ftype = case ftype of
|
|
Packit |
8cecbd |
NormalFile {} -> True
|
|
Packit |
8cecbd |
HardLink {} -> True
|
|
Packit |
8cecbd |
SymbolicLink {} -> True
|
|
Packit |
8cecbd |
Directory -> True
|
|
Packit |
8cecbd |
_ -> False
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
portableChar c = c <= '\127'
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
-- | Portability problems in a tar archive
|
|
Packit |
8cecbd |
data PortabilityError
|
|
Packit |
8cecbd |
= NonPortableFormat Format
|
|
Packit |
8cecbd |
| NonPortableFileType
|
|
Packit |
8cecbd |
| NonPortableEntryNameChar FilePath
|
|
Packit |
8cecbd |
| NonPortableFileName PortabilityPlatform FileNameError
|
|
Packit |
8cecbd |
deriving (Typeable)
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
-- | The name of a platform that portability issues arise from
|
|
Packit |
8cecbd |
type PortabilityPlatform = String
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
instance Exception PortabilityError
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
instance Show PortabilityError where
|
|
Packit |
8cecbd |
show (NonPortableFormat format) = "Archive is in the " ++ fmt ++ " format"
|
|
Packit |
8cecbd |
where fmt = case format of V7Format -> "old Unix V7 tar"
|
|
Packit |
8cecbd |
UstarFormat -> "ustar" -- I never generate this but a user might
|
|
Packit |
8cecbd |
GnuFormat -> "GNU tar"
|
|
Packit |
8cecbd |
show NonPortableFileType = "Non-portable file type in archive"
|
|
Packit |
8cecbd |
show (NonPortableEntryNameChar posixPath)
|
|
Packit |
8cecbd |
= "Non-portable character in archive entry name: " ++ show posixPath
|
|
Packit |
8cecbd |
show (NonPortableFileName platform err)
|
|
Packit |
8cecbd |
= showFileNameError (Just platform) err
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
--------------------------
|
|
Packit |
8cecbd |
-- Utils
|
|
Packit |
8cecbd |
--
|
|
Packit |
8cecbd |
|
|
Packit |
8cecbd |
checkEntries :: (Entry -> Maybe e') -> Entries e -> Entries (Either e e')
|
|
Packit |
8cecbd |
checkEntries checkEntry =
|
|
Packit |
8cecbd |
mapEntries (\entry -> maybe (Right entry) Left (checkEntry entry))
|