1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
|
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-orphans #-}
{-# OPTIONS_HADDOCK hide #-}
-----------------------------------------------------------------------------
-- |
-- Module : Codec.Archive.Tar.Check.Internal
-- Copyright : (c) 2008-2012 Duncan Coutts
-- 2011 Max Bolingbroke
-- License : BSD3
--
-- Maintainer : duncan@community.haskell.org
-- Portability : portable
--
-- Perform various checks on tar file entries.
--
-----------------------------------------------------------------------------
module Codec.Archive.Tar.Check.Internal (
-- * Security
checkSecurity,
checkEntrySecurity,
FileNameError(..),
-- * Tarbombs
checkTarbomb,
checkEntryTarbomb,
TarBombError(..),
-- * Portability
checkPortability,
checkEntryPortability,
PortabilityError(..),
PortabilityPlatform,
) where
import Codec.Archive.Tar.LongNames
import Codec.Archive.Tar.Types
import Control.Applicative ((<|>))
import qualified Data.ByteString.Lazy.Char8 as Char8
import Data.Maybe (fromMaybe)
import Data.Typeable (Typeable)
import Control.Exception (Exception(..))
import qualified System.FilePath as FilePath.Native
( splitDirectories, isAbsolute, isValid, (</>), takeDirectory, hasDrive )
import qualified System.FilePath.Windows as FilePath.Windows
import qualified System.FilePath.Posix as FilePath.Posix
--------------------------
-- Security
--
-- | This function checks a sequence of tar entries for file name security
-- problems. It checks that:
--
-- * file paths are not absolute
--
-- * file paths do not refer outside of the archive
--
-- * file names are valid
--
-- These checks are from the perspective of the current OS. That means we check
-- for \"@C:\blah@\" files on Windows and \"\/blah\" files on Unix. For archive
-- entry types 'HardLink' and 'SymbolicLink' the same checks are done for the
-- link target. A failure in any entry terminates the sequence of entries with
-- an error.
--
-- Whenever possible, consider fusing 'Codec.Archive.Tar.Check.checkSecurity'
-- with packing / unpacking by using
-- 'Codec.Archive.Tar.packAndCheck' / 'Codec.Archive.Tar.unpackAndCheck'
-- with 'Codec.Archive.Tar.Check.checkEntrySecurity'.
-- Not only it is faster, but also alleviates issues with lazy I/O
-- such as exhaustion of file handlers.
checkSecurity
:: Entries e
-> GenEntries FilePath FilePath (Either (Either e DecodeLongNamesError) FileNameError)
checkSecurity = checkEntries checkEntrySecurity . decodeLongNames
-- | Worker of 'Codec.Archive.Tar.Check.checkSecurity'.
--
-- @since 0.6.0.0
checkEntrySecurity :: GenEntry FilePath FilePath -> Maybe FileNameError
checkEntrySecurity e =
check (entryTarPath e) <|>
case entryContent e of
HardLink link ->
check link
SymbolicLink link ->
check (FilePath.Posix.takeDirectory (entryTarPath e) FilePath.Posix.</> link)
_ -> Nothing
where
checkPosix name
| FilePath.Posix.isAbsolute name
= Just $ AbsoluteFileName name
| not (FilePath.Posix.isValid name)
= Just $ InvalidFileName name
| not (isInsideBaseDir (FilePath.Posix.splitDirectories name))
= Just $ UnsafeLinkTarget name
| otherwise = Nothing
checkNative (fromFilePathToNative -> name)
| FilePath.Native.isAbsolute name || FilePath.Native.hasDrive name
= Just $ AbsoluteFileName name
| not (FilePath.Native.isValid name)
= Just $ InvalidFileName name
| not (isInsideBaseDir (FilePath.Native.splitDirectories name))
= Just $ UnsafeLinkTarget name
| otherwise = Nothing
check name = checkPosix name <|> checkNative (fromFilePathToNative name)
isInsideBaseDir :: [FilePath] -> Bool
isInsideBaseDir = go 0
where
go :: Word -> [FilePath] -> Bool
go !_ [] = True
go 0 (".." : _) = False
go lvl (".." : xs) = go (lvl - 1) xs
go lvl ("." : xs) = go lvl xs
go lvl (_ : xs) = go (lvl + 1) xs
-- | Errors arising from tar file names being in some way invalid or dangerous
data FileNameError
= InvalidFileName FilePath
| AbsoluteFileName FilePath
| UnsafeLinkTarget FilePath
-- ^ @since 0.6.0.0
deriving (Typeable)
instance Show FileNameError where
show = showFileNameError Nothing
instance Exception FileNameError
showFileNameError :: Maybe PortabilityPlatform -> FileNameError -> String
showFileNameError mb_plat err = case err of
InvalidFileName path -> "Invalid" ++ plat ++ " file name in tar archive: " ++ show path
AbsoluteFileName path -> "Absolute" ++ plat ++ " file name in tar archive: " ++ show path
UnsafeLinkTarget path -> "Unsafe" ++ plat ++ " link target in tar archive: " ++ show path
where plat = maybe "" (' ':) mb_plat
--------------------------
-- Tarbombs
--
-- | This function checks a sequence of tar entries for being a \"tar bomb\".
-- This means that the tar file does not follow the standard convention that
-- all entries are within a single subdirectory, e.g. a file \"foo.tar\" would
-- usually have all entries within the \"foo/\" subdirectory.
--
-- Given the expected subdirectory, this function checks all entries are within
-- that subdirectroy.
--
-- Note: This check must be used in conjunction with 'Codec.Archive.Tar.Check.checkSecurity'
-- (or 'Codec.Archive.Tar.Check.checkPortability').
--
-- Whenever possible, consider fusing 'Codec.Archive.Tar.Check.checkTarbomb'
-- with packing / unpacking by using
-- 'Codec.Archive.Tar.packAndCheck' / 'Codec.Archive.Tar.unpackAndCheck'
-- with 'Codec.Archive.Tar.Check.checkEntryTarbomb'.
-- Not only it is faster, but also alleviates issues with lazy I/O
-- such as exhaustion of file handlers.
checkTarbomb
:: FilePath
-> Entries e
-> GenEntries FilePath FilePath (Either (Either e DecodeLongNamesError) TarBombError)
checkTarbomb expectedTopDir
= checkEntries (checkEntryTarbomb expectedTopDir)
. decodeLongNames
-- | Worker of 'checkTarbomb'.
--
-- @since 0.6.0.0
checkEntryTarbomb :: FilePath -> GenEntry FilePath linkTarget -> Maybe TarBombError
checkEntryTarbomb expectedTopDir entry = do
case entryContent entry of
-- Global extended header aka XGLTYPE aka pax_global_header
-- https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_02
OtherEntryType 'g' _ _ -> Nothing
-- Extended header referring to the next file in the archive aka XHDTYPE
OtherEntryType 'x' _ _ -> Nothing
_ ->
case FilePath.Posix.splitDirectories (entryTarPath entry) of
(topDir:_) | topDir == expectedTopDir -> Nothing
_ -> Just $ TarBombError expectedTopDir (entryTarPath entry)
-- | An error that occurs if a tar file is a \"tar bomb\" that would extract
-- files outside of the intended directory.
data TarBombError
= TarBombError
FilePath -- ^ Path inside archive.
--
-- @since 0.6.0.0
FilePath -- ^ Expected top directory.
deriving (Typeable)
instance Exception TarBombError
instance Show TarBombError where
show (TarBombError expectedTopDir tarBombPath)
= "File in tar archive, " ++ show tarBombPath ++
", is not in the expected directory " ++ show expectedTopDir
--------------------------
-- Portability
--
-- | This function checks a sequence of tar entries for a number of portability
-- issues. It will complain if:
--
-- * The old \"Unix V7\" or \"gnu\" formats are used. For maximum portability
-- only the POSIX standard \"ustar\" format should be used.
--
-- * A non-portable entry type is used. Only ordinary files, hard links,
-- symlinks and directories are portable. Device files, pipes and others are
-- not portable between all common operating systems.
--
-- * Non-ASCII characters are used in file names. There is no agreed portable
-- convention for Unicode or other extended character sets in file names in
-- tar archives.
--
-- * File names that would not be portable to both Unix and Windows. This check
-- includes characters that are valid in both systems and the \'/\' vs \'\\\'
-- directory separator conventions.
--
-- Whenever possible, consider fusing 'checkPortability' with packing / unpacking by using
-- 'Codec.Archive.Tar.packAndCheck' / 'Codec.Archive.Tar.unpackAndCheck'
-- with 'checkEntryPortability'.
-- Not only it is faster, but also alleviates issues with lazy I/O
-- such as exhaustion of file handlers.
checkPortability
:: Entries e
-> GenEntries FilePath FilePath (Either (Either e DecodeLongNamesError) PortabilityError)
checkPortability = checkEntries checkEntryPortability . decodeLongNames
-- | Worker of 'checkPortability'.
--
-- @since 0.6.0.0
checkEntryPortability :: GenEntry FilePath linkTarget -> Maybe PortabilityError
checkEntryPortability entry
| entryFormat entry `elem` [V7Format, GnuFormat]
= Just $ NonPortableFormat (entryFormat entry)
| not (portableFileType (entryContent entry))
= Just NonPortableFileType
| not (all portableChar posixPath)
= Just $ NonPortableEntryNameChar posixPath
| not (FilePath.Posix.isValid posixPath)
= Just $ NonPortableFileName "unix" (InvalidFileName posixPath)
| not (FilePath.Windows.isValid windowsPath)
= Just $ NonPortableFileName "windows" (InvalidFileName windowsPath)
| FilePath.Posix.isAbsolute posixPath
= Just $ NonPortableFileName "unix" (AbsoluteFileName posixPath)
| FilePath.Windows.isAbsolute windowsPath
= Just $ NonPortableFileName "windows" (AbsoluteFileName windowsPath)
| any (=="..") (FilePath.Posix.splitDirectories posixPath)
= Just $ NonPortableFileName "unix" (InvalidFileName posixPath)
| any (=="..") (FilePath.Windows.splitDirectories windowsPath)
= Just $ NonPortableFileName "windows" (InvalidFileName windowsPath)
| otherwise = Nothing
where
posixPath = entryTarPath entry
windowsPath = fromFilePathToWindowsPath posixPath
portableFileType ftype = case ftype of
NormalFile {} -> True
HardLink {} -> True
SymbolicLink {} -> True
Directory -> True
_ -> False
portableChar c = c <= '\127'
-- | Portability problems in a tar archive
data PortabilityError
= NonPortableFormat Format
| NonPortableFileType
| NonPortableEntryNameChar FilePath
| NonPortableFileName PortabilityPlatform FileNameError
deriving (Typeable)
-- | The name of a platform that portability issues arise from
type PortabilityPlatform = String
instance Exception PortabilityError
instance Show PortabilityError where
show (NonPortableFormat format) = "Archive is in the " ++ fmt ++ " format"
where fmt = case format of V7Format -> "old Unix V7 tar"
UstarFormat -> "ustar" -- I never generate this but a user might
GnuFormat -> "GNU tar"
show NonPortableFileType = "Non-portable file type in archive"
show (NonPortableEntryNameChar posixPath)
= "Non-portable character in archive entry name: " ++ show posixPath
show (NonPortableFileName platform err)
= showFileNameError (Just platform) err
--------------------------
-- Utils
checkEntries
:: (GenEntry tarPath linkTarget -> Maybe e')
-> GenEntries tarPath linkTarget e
-> GenEntries tarPath linkTarget (Either e e')
checkEntries checkEntry =
mapEntries (\entry -> maybe (Right entry) Left (checkEntry entry))
|