CVE-2023-38745 (published 2023-07-25T04:15Z)

Pandoc before 3.1.6 allows arbitrary file write: this can be triggered by providing a crafted image element in the input when generating files via the --extract-media option or outputting to PDF format. This allows an attacker to create or overwrite arbitrary files, depending on the privileges of the process running Pandoc. It only affects systems that pass untrusted user input to Pandoc and allow Pandoc to be used to produce a PDF or with the --extract-media option. NOTE: this issue exists because of an incomplete fix for CVE-2023-35936 (failure to properly account for double encoded path names).

CVSS v3 base score: 6.3, MEDIUM (CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:H)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/jgm/pandoc
commit_details: repo_url: https://github.com/jgm/pandoc
author: John MacFarlane
author_date: 2023-07-20 09:26:38-07:00
msg: Fix new variant of the vulnerability in CVE-2023-35936.

Guilhem Moulin noticed that the fix to CVE-2023-35936 was incomplete.
An attacker could get around it by double-encoding the malicious
extension to create or override arbitrary files.

    $ echo '![](data://image/png;base64,cHJpbnQgImhlbGxvIgo=;.lua+%252f%252e%252e%252f%252e%252e%252fb%252elua)' >b.md
    $ .cabal/bin/pandoc b.md --extract-media=bar
    <p><img
    src="bar/2a0eaa89f43fada3e6c577beea4f2f8f53ab6a1d.lua+%2f%2e%2e%2f%2e%2e%2fb%2elua" /></p>
    $ cat b.lua
    print "hello"
    $ find bar
    bar/
    bar/2a0eaa89f43fada3e6c577beea4f2f8f53ab6a1d.lua+

This commit adds a test case for this more complex attack and fixes
the vulnerability.  (The fix is quite simple: if the URL-unescaped
filename or extension contains a '%', we just use the sha1 hash of the
contents as the canonical name, just as we do if the filename contains
'..'.)
repository: repo_url: https://github.com/jgm/pandoc
repo_name: jgm/pandoc
description: Universal markup converter
repo_language: Haskell
owner: jgm
file_changes: filename: IO.hs
change_type: ModificationType.MODIFY
diff: @@ -224,6 +224,8 @@ writeMedia :: (PandocMonad m, MonadIO m)
            -> m ()
 writeMedia dir (fp, _mt, bs) = do
   -- we normalize to get proper path separators for the platform
+  -- we unescape URI encoding, but given how insertMedia
+  -- is written, we shouldn't have any % in a canonical media name...
   let fullpath = normalise $ dir </> unEscapeString fp
   liftIOError (createDirectoryIfMissing True) (takeDirectory fullpath)
   report $ Extracting (T.pack fullpath)

programming_language: Haskell
filename: MediaBag.hs
change_type: ModificationType.MODIFY
diff: @@ -90,16 +90,17 @@ insertMedia fp mbMime contents (MediaBag mediamap) =
                        && Windows.isRelative fp''
                        && isNothing uri
                        && not (".." `isInfixOf` fp'')
+                       && '%' `notElem` fp''
                      then fp''
-                     else showDigest (sha1 contents) <> "." <> ext
+                     else showDigest (sha1 contents) <> ext
         fallback = case takeExtension fp'' of
                         ".gz" -> getMimeTypeDef $ dropExtension fp''
                         _     -> getMimeTypeDef fp''
         mt = fromMaybe fallback mbMime
         path = maybe fp'' (unEscapeString . uriPath) uri
         ext = case takeExtension path of
-                '.':e -> e
-                _ -> maybe "" T.unpack $ extensionFromMimeType mt
+                '.':e | '%' `notElem` e -> '.':e
+                _ -> maybe "" (\x -> '.':T.unpack x) $ extensionFromMimeType mt
 
 -- | Lookup a media item in a 'MediaBag', returning mime type and contents.
 lookupMedia :: FilePath

programming_language: Haskell
filename: MediaBag.hs
change_type: ModificationType.MODIFY
diff: @@ -20,7 +20,7 @@ tests = [
         let d = B.doc $
                   B.para (B.image "../../test/lalune.jpg" "" mempty) <>
                   B.para (B.image "moon.jpg" "" mempty) <>
-                  B.para (B.image "data://image/png;base64,cHJpbnQgImhlbGxvIgo=;.lua+%2f%2e%2e%2f%2e%2e%2fa%2elua" "" mempty) <>
+                  B.para (B.image "data:image/png;base64,cHJpbnQgImhlbGxvIgo=;.lua+%2f%2e%2e%2f%2e%2e%2fa%2elua" "" mempty) <>
                   B.para (B.image "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" "" mempty)
         runIOorExplode $ do
           fillMediaBag d
@@ -35,4 +35,14 @@ tests = [
           (exists3 && not exists4)
         exists5 <- doesFileExist ("foo" </> "d5fceb6532643d0d84ffe09c40c481ecdf59e15a.gif")
         assertBool "data uri with gif is not properly decoded" exists5
+        -- double-encoded version:
+        let e = B.doc $
+                  B.para (B.image "data:image/png;base64,cHJpbnQgInB3bmVkIgo=;.lua+%252f%252e%252e%252f%252e%252e%252fb%252elua" "" mempty)
+        runIOorExplode $ do
+          fillMediaBag e
+          extractMedia "bar" e
+        exists6 <- doesFileExist ("bar" </> "772ceca21a2751863ec46cb23db0e7fc35b9cff8.png")
+        exists7 <- doesFileExist "b.lua"
+        assertBool "data uri with double-encoded malicious payload gets written outside of destination dir"
+          (exists6 && not exists7)
   ]

programming_language: Haskell