Skip to content

Commit a0c7c6f

Browse files
committed
Rename security checks
1 parent 733c62e commit a0c7c6f

File tree

7 files changed

+104
-61
lines changed

7 files changed

+104
-61
lines changed

Codec/Archive/Tar.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ module Codec.Archive.Tar (
108108
pack,
109109
packAndCheck,
110110
unpack,
111-
unpackWith,
111+
unpackAndCheck,
112112

113113
-- * Types
114114
-- ** Tar entry type

Codec/Archive/Tar/Check.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,17 @@ module Codec.Archive.Tar.Check (
1515

1616
-- * Security
1717
checkSecurity,
18+
checkEntrySecurity,
1819
FileNameError(..),
1920

2021
-- * Tarbombs
2122
checkTarbomb,
23+
checkEntryTarbomb,
2224
TarBombError(..),
2325

2426
-- * Portability
2527
checkPortability,
28+
checkEntryPortability,
2629
PortabilityError(..),
2730
PortabilityPlatform,
2831
) where

Codec/Archive/Tar/Check/Internal.hs

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,22 @@ module Codec.Archive.Tar.Check.Internal (
2222

2323
-- * Security
2424
checkSecurity,
25+
checkEntrySecurity,
2526
FileNameError(..),
2627

2728
-- * Tarbombs
2829
checkTarbomb,
30+
checkEntryTarbomb,
2931
TarBombError(..),
3032

3133
-- * Portability
3234
checkPortability,
35+
checkEntryPortability,
3336
PortabilityError(..),
3437
PortabilityPlatform,
3538
) where
3639

40+
import Codec.Archive.Tar.LongNames
3741
import Codec.Archive.Tar.Types
3842
import Control.Applicative ((<|>))
3943
import Control.Monad.Catch (MonadThrow(throwM))
@@ -67,35 +71,42 @@ import qualified System.FilePath.Posix as FilePath.Posix
6771
-- link target. A failure in any entry terminates the sequence of entries with
6872
-- an error.
6973
--
70-
checkSecurity :: CheckSecurityCallback
71-
checkSecurity e = do
72-
check (entryTarPath e)
74+
checkSecurity
75+
:: Entries e
76+
-> GenEntries FilePath FilePath (Either (Either e DecodeLongNamesError) FileNameError)
77+
checkSecurity = checkEntries checkEntrySecurity . decodeLongNames
78+
79+
-- |
80+
-- @since 0.6.0.0
81+
checkEntrySecurity :: GenEntry FilePath FilePath -> Maybe FileNameError
82+
checkEntrySecurity e =
83+
check (entryTarPath e) <|>
7384
case entryContent e of
7485
HardLink link ->
7586
check link
7687
SymbolicLink link ->
7788
check (FilePath.Posix.takeDirectory (entryTarPath e) FilePath.Posix.</> link)
78-
_ -> pure ()
89+
_ -> Nothing
7990
where
8091
checkPosix name
8192
| FilePath.Posix.isAbsolute name
82-
= throwM $ AbsoluteFileName name
93+
= Just $ AbsoluteFileName name
8394
| not (FilePath.Posix.isValid name)
84-
= throwM $ InvalidFileName name
95+
= Just $ InvalidFileName name
8596
| not (isInsideBaseDir (FilePath.Posix.splitDirectories name))
86-
= throwM $ UnsafeLinkTarget name
87-
| otherwise = pure ()
97+
= Just $ UnsafeLinkTarget name
98+
| otherwise = Nothing
8899

89100
checkNative (fromFilePathToNative -> name)
90101
| FilePath.Native.isAbsolute name || FilePath.Native.hasDrive name
91-
= throwM $ AbsoluteFileName name
102+
= Just $ AbsoluteFileName name
92103
| not (FilePath.Native.isValid name)
93-
= throwM $ InvalidFileName name
104+
= Just $ InvalidFileName name
94105
| not (isInsideBaseDir (FilePath.Native.splitDirectories name))
95-
= throwM $ UnsafeLinkTarget name
96-
| otherwise = pure ()
106+
= Just $ UnsafeLinkTarget name
107+
| otherwise = Nothing
97108

98-
check name = checkPosix name >>= \_ -> checkNative (fromFilePathToNative name)
109+
check name = checkPosix name <|> checkNative (fromFilePathToNative name)
99110

100111
isInsideBaseDir :: [FilePath] -> Bool
101112
isInsideBaseDir = go 0
@@ -143,18 +154,28 @@ showFileNameError mb_plat err = case err of
143154
-- Note: This check must be used in conjunction with 'checkSecurity'
144155
-- (or 'checkPortability').
145156
--
146-
checkTarbomb :: FilePath -> CheckSecurityCallback
147-
checkTarbomb expectedTopDir entry = do
157+
checkTarbomb
158+
:: FilePath
159+
-> Entries e
160+
-> GenEntries FilePath FilePath (Either (Either e DecodeLongNamesError) TarBombError)
161+
checkTarbomb expectedTopDir
162+
= checkEntries (checkEntryTarbomb expectedTopDir)
163+
. decodeLongNames
164+
165+
-- |
166+
-- @since 0.6.0.0
167+
checkEntryTarbomb :: FilePath -> GenEntry FilePath linkTarget -> Maybe TarBombError
168+
checkEntryTarbomb expectedTopDir entry = do
148169
case entryContent entry of
149170
-- Global extended header aka XGLTYPE aka pax_global_header
150171
-- https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_02
151-
OtherEntryType 'g' _ _ -> pure ()
172+
OtherEntryType 'g' _ _ -> Nothing
152173
-- Extended header referring to the next file in the archive aka XHDTYPE
153-
OtherEntryType 'x' _ _ -> pure ()
174+
OtherEntryType 'x' _ _ -> Nothing
154175
_ ->
155176
case FilePath.Posix.splitDirectories (entryTarPath entry) of
156-
(topDir:_) | topDir == expectedTopDir -> pure ()
157-
_ -> throwM $ TarBombError expectedTopDir (entryTarPath entry)
177+
(topDir:_) | topDir == expectedTopDir -> Nothing
178+
_ -> Just $ TarBombError expectedTopDir (entryTarPath entry)
158179

159180
-- | An error that occurs if a tar file is a \"tar bomb\" that would extract
160181
-- files outside of the intended directory.
@@ -195,33 +216,40 @@ instance Show TarBombError where
195216
-- includes characters that are valid in both systems and the \'/\' vs \'\\\'
196217
-- directory separator conventions.
197218
--
198-
checkPortability :: CheckSecurityCallback
199-
checkPortability entry
219+
checkPortability
220+
:: Entries e
221+
-> GenEntries FilePath FilePath (Either (Either e DecodeLongNamesError) PortabilityError)
222+
checkPortability = checkEntries checkEntryPortability . decodeLongNames
223+
224+
-- |
225+
-- @since 0.6.0.0
226+
checkEntryPortability :: GenEntry FilePath linkTarget -> Maybe PortabilityError
227+
checkEntryPortability entry
200228
| entryFormat entry `elem` [V7Format, GnuFormat]
201-
= throwM $ NonPortableFormat (entryFormat entry)
229+
= Just $ NonPortableFormat (entryFormat entry)
202230

203231
| not (portableFileType (entryContent entry))
204-
= throwM NonPortableFileType
232+
= Just NonPortableFileType
205233

206234
| not (all portableChar posixPath)
207-
= throwM $ NonPortableEntryNameChar posixPath
235+
= Just $ NonPortableEntryNameChar posixPath
208236

209237
| not (FilePath.Posix.isValid posixPath)
210-
= throwM $ NonPortableFileName "unix" (InvalidFileName posixPath)
238+
= Just $ NonPortableFileName "unix" (InvalidFileName posixPath)
211239
| not (FilePath.Windows.isValid windowsPath)
212-
= throwM $ NonPortableFileName "windows" (InvalidFileName windowsPath)
240+
= Just $ NonPortableFileName "windows" (InvalidFileName windowsPath)
213241

214242
| FilePath.Posix.isAbsolute posixPath
215-
= throwM $ NonPortableFileName "unix" (AbsoluteFileName posixPath)
243+
= Just $ NonPortableFileName "unix" (AbsoluteFileName posixPath)
216244
| FilePath.Windows.isAbsolute windowsPath
217-
= throwM $ NonPortableFileName "windows" (AbsoluteFileName windowsPath)
245+
= Just $ NonPortableFileName "windows" (AbsoluteFileName windowsPath)
218246

219247
| any (=="..") (FilePath.Posix.splitDirectories posixPath)
220-
= throwM $ NonPortableFileName "unix" (InvalidFileName posixPath)
248+
= Just $ NonPortableFileName "unix" (InvalidFileName posixPath)
221249
| any (=="..") (FilePath.Windows.splitDirectories windowsPath)
222-
= throwM $ NonPortableFileName "windows" (InvalidFileName windowsPath)
250+
= Just $ NonPortableFileName "windows" (InvalidFileName windowsPath)
223251

224-
| otherwise = pure ()
252+
| otherwise = Nothing
225253

226254
where
227255
posixPath = entryTarPath entry
@@ -259,3 +287,10 @@ instance Show PortabilityError where
259287
= "Non-portable character in archive entry name: " ++ show posixPath
260288
show (NonPortableFileName platform err)
261289
= showFileNameError (Just platform) err
290+
291+
--------------------------
292+
-- Utils
293+
294+
checkEntries :: (GenEntry a b -> Maybe e') -> GenEntries a b e -> GenEntries a b (Either e e')
295+
checkEntries checkEntry =
296+
mapEntries (\entry -> maybe (Right entry) Left (checkEntry entry))

Codec/Archive/Tar/Pack.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import System.IO
4848
( IOMode(ReadMode), openBinaryFile, hFileSize )
4949
import System.IO.Unsafe (unsafeInterleaveIO)
5050
import Control.Exception (throwIO, SomeException)
51-
import Codec.Archive.Tar.Check.Internal (checkSecurity)
51+
import Codec.Archive.Tar.Check.Internal (checkEntrySecurity)
5252

5353
-- | Creates a tar archive from a list of directory or files. Any directories
5454
-- specified will have their contents included recursively. Paths in the
@@ -69,21 +69,21 @@ pack
6969
:: FilePath -- ^ Base directory
7070
-> [FilePath] -- ^ Files and directories to pack, relative to the base dir
7171
-> IO [Entry]
72-
pack = packAndCheck (const $ pure ())
72+
pack = packAndCheck (const Nothing)
7373

7474
-- | Like 'pack', but allows to specify any sanity/security checks on the input
7575
-- filenames.
7676
--
7777
-- @since 0.6.0.0
7878
packAndCheck
79-
:: CheckSecurityCallback
79+
:: (GenEntry FilePath FilePath -> Maybe SomeException)
8080
-> FilePath -- ^ Base directory
8181
-> [FilePath] -- ^ Files and directories to pack, relative to the base dir
8282
-> IO [Entry]
8383
packAndCheck secCB baseDir relpaths = do
8484
paths <- preparePaths baseDir relpaths
8585
entries <- packPaths baseDir paths
86-
traverse_ secCB entries
86+
traverse_ (maybe (pure ()) throwIO . secCB) entries
8787
pure $ concatMap encodeLongNames entries
8888

8989
preparePaths :: FilePath -> [FilePath] -> IO [FilePath]

Codec/Archive/Tar/Types.hs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ module Codec.Archive.Tar.Types (
2929
DevMajor,
3030
DevMinor,
3131
Format(..),
32-
CheckSecurityCallback,
3332

3433
simpleEntry,
3534
longLinkEntry,
@@ -353,7 +352,7 @@ instance Show TarPath where
353352
--
354353
-- * The tar path may be an absolute path or may contain @\"..\"@ components.
355354
-- For security reasons this should not usually be allowed, but it is your
356-
-- responsibility to check for these conditions (eg using 'checkSecurity').
355+
-- responsibility to check for these conditions (eg using 'checkEntrySecurity').
357356
--
358357
fromTarPath :: TarPath -> FilePath
359358
fromTarPath = BS.Char8.unpack . fromTarPathInternal FilePath.Native.pathSeparator
@@ -611,7 +610,7 @@ foldEntries next done fail' = fold
611610
-- value.
612611
--
613612
foldlEntries :: (a -> Entry -> a) -> a -> Entries e -> Either (e, a) a
614-
foldlEntries f z = go z
613+
foldlEntries f = go
615614
where
616615
go !acc (Next e es) = go (f acc e) es
617616
go !acc Done = Right acc
@@ -622,15 +621,18 @@ foldlEntries f z = go z
622621
--
623622
-- If your mapping function cannot fail it may be more convenient to use
624623
-- 'mapEntriesNoFail'
625-
mapEntries :: (Entry -> Either e' Entry) -> Entries e -> Entries (Either e e')
624+
mapEntries
625+
:: (GenEntry tarPath linkTarget -> Either e' (GenEntry tarPath linkTarget))
626+
-> GenEntries tarPath linkTarget e
627+
-> GenEntries tarPath linkTarget (Either e e')
626628
mapEntries f =
627-
foldEntries (\entry rest -> either (Fail . Right) (flip Next rest) (f entry)) Done (Fail . Left)
629+
foldEntries (\entry rest -> either (Fail . Right) (`Next` rest) (f entry)) Done (Fail . Left)
628630

629631
-- | Like 'mapEntries' but the mapping function itself cannot fail.
630632
--
631633
mapEntriesNoFail :: (Entry -> Entry) -> Entries e -> Entries e
632634
mapEntriesNoFail f =
633-
foldEntries (\entry -> Next (f entry)) Done Fail
635+
foldEntries (Next . f) Done Fail
634636

635637
-- | @since 0.5.1.0
636638
instance Sem.Semigroup (GenEntries tarPath linkTarget e) where
@@ -644,10 +646,3 @@ instance NFData e => NFData (GenEntries tarPath linkTarget e) where
644646
rnf (Next e es) = rnf e `seq` rnf es
645647
rnf Done = ()
646648
rnf (Fail e) = rnf e
647-
648-
-- | @since 0.6.0.0
649-
type CheckSecurityCallback =
650-
forall m.
651-
MonadThrow m
652-
=> GenEntry FilePath FilePath
653-
-> m ()

Codec/Archive/Tar/Unpack.hs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
{-# LANGUAGE ViewPatterns #-}
33
{-# LANGUAGE ScopedTypeVariables #-}
44
{-# LANGUAGE RankNTypes #-}
5+
6+
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
7+
{-# HLINT ignore "Use for_" #-}
8+
59
-----------------------------------------------------------------------------
610
-- |
711
-- Module : Codec.Archive.Tar
@@ -16,7 +20,7 @@
1620
-----------------------------------------------------------------------------
1721
module Codec.Archive.Tar.Unpack (
1822
unpack,
19-
unpackWith,
23+
unpackAndCheck,
2024
) where
2125

2226
import Codec.Archive.Tar.Types
@@ -57,9 +61,7 @@ import GHC.IO.Exception (IOErrorType(InappropriateType, IllegalOperation, Permis
5761
import Data.Time.Clock.POSIX
5862
( posixSecondsToUTCTime )
5963
import Control.Exception as Exception
60-
( catch )
61-
62-
64+
( catch, SomeException(..) )
6365

6466
-- | Create local files and directories based on the entries of a tar archive.
6567
--
@@ -76,18 +78,23 @@ import Control.Exception as Exception
7678
-- into an empty directory so that you can easily clean up if unpacking fails
7779
-- part-way.
7880
--
79-
-- On its own, this function only checks for security (using 'checkSecurity').
80-
-- Use 'unpackWith' if you need more checks.
81+
-- On its own, this function only checks for security (using 'checkEntrySecurity').
82+
-- Use 'unpackAndCheck' if you need more checks.
8183
--
8284
unpack :: Exception e => FilePath -> Entries e -> IO ()
83-
unpack = unpackWith checkSecurity
85+
unpack = unpackAndCheck (fmap SomeException . checkEntrySecurity)
8486

8587
-- | Like 'unpack', but does not perform any sanity/security checks on the tar entries.
86-
-- You can do so yourself, e.g.: @unpackRaw@ 'checkSecurity' @dir@ @entries@.
88+
-- You can do so yourself, e.g.: @unpackRaw@ 'checkEntrySecurity' @dir@ @entries@.
8789
--
8890
-- @since 0.6.0.0
89-
unpackWith :: Exception e => CheckSecurityCallback -> FilePath -> Entries e -> IO ()
90-
unpackWith secCB baseDir entries = do
91+
unpackAndCheck
92+
:: Exception e
93+
=> (GenEntry FilePath FilePath -> Maybe SomeException)
94+
-> FilePath
95+
-> Entries e
96+
-> IO ()
97+
unpackAndCheck secCB baseDir entries = do
9198
let resolvedEntries = decodeLongNames entries
9299
uEntries <- unpackEntries [] resolvedEntries
93100
let (hardlinks, symlinks) = partition (\(_, _, x) -> x) uEntries
@@ -108,7 +115,10 @@ unpackWith secCB baseDir entries = do
108115
unpackEntries _ (Fail err) = either throwIO throwIO err
109116
unpackEntries links Done = return links
110117
unpackEntries links (Next entry es) = do
111-
secCB entry
118+
case secCB entry of
119+
Nothing -> pure ()
120+
Just e -> throwIO e
121+
112122
case entryContent entry of
113123
NormalFile file _ -> do
114124
extractFile (entryPermissions entry) (entryTarPath entry) file (entryTime entry)

changelog.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ See also http://pvp.haskell.org/faq
1818
* Switch to trailer parsing mode only after a full block of `NUL`
1919
* Drop deprecated `emptyIndex` and `finaliseIndex`
2020
* Extend `FileNameError` with `UnsafeLinkTarget` constructor
21-
* Add `CheckSecurityCallback`, `packWith`, `unpackWith`
21+
* Add `packAndCheck` and `unpackAndCheck`
2222
* Generalize `Entries`, `Entry` and `EntryContent` to `GenEntries`, `GenEntry` and `GenEntryContent`
2323

2424
0.5.1.1 Herbert Valerio Riedel <[email protected]> August 2019

0 commit comments

Comments
 (0)