Blame Codec/Archive/Tar/Check.hs

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))