CVE-2020-36326: Deserialization of Untrusted Data (CWE-502)
value: PHPMailer 6.1.8 through 6.4.0 allows object injection through Phar Deserialization via addAttachment with a UNC pathname. NOTE: this is similar to CVE-2018-19296, but arose because 6.1.8 fixed a functionality problem in which UNC pathnames were always considered unreadable by PHPMailer, even in safe contexts. As an unintended side effect, this fix eliminated the code that blocked addAttachment exploitation.
CWE description: The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
CVSS v3 base score: 9.8
Published: 2021-04-28T03:15Z
Repository: https://github.com/PHPMailer/PHPMailer
Language: PHP
Files changed: src/PHPMailer.php, test/PHPMailerTest.php
Commit date: 2020-09-30 19:29:00 +0200
Commit message: Proposed fix for #2069

Diff: diff --git a/src/PHPMailer.php b/src/PHPMailer.php
index ac5f041..1448ce2 100644
--- a/src/PHPMailer.php
+++ b/src/PHPMailer.php
@@ -1751,10 +1751,27 @@ class PHPMailer
     protected static function isPermittedPath($path)
     {
         return !preg_match('#^[a-z]+://#i', $path);
     }
 
+    /**
+     * Check whether a file path is safe, accessible, and readable.
+     *
+     * @param string $path A relative or absolute path to a file
+     *
+     * @return bool
+     */
+    protected static function fileIsAccessible($path)
+    {
+        $readable = file_exists($path);
+        //If not a UNC path (expected to start with \\), check read permission, see #2069
+        if (strpos($path, '\\\\') !== 0) {
+            $readable = $readable && is_readable($path);
+        }
+        return static::isPermittedPath($path) && $readable;
+    }
+
     /**
      * Send mail using the PHP mail() function.
      *
      * @see http://www.php.net/manual/en/book.mail.php
      *
@@ -2139,11 +2156,11 @@ class PHPMailer
         $foundlang = true;
         $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php';
         // There is no English translation file
         if ('en' !== $langcode) {
             // Make sure language file path is readable
-            if (!static::isPermittedPath($lang_file) || !file_exists($lang_file)) {
+            if (!static::fileIsAccessible($lang_file)) {
                 $foundlang = false;
             } else {
                 // Overwrite language-specific strings.
                 // This way we'll never have missing translation keys.
                 $foundlang = include $lang_file;
@@ -2968,11 +2985,11 @@ class PHPMailer
         $encoding = self::ENCODING_BASE64,
         $type = '',
         $disposition = 'attachment'
     ) {
         try {
-            if (!static::isPermittedPath($path) || !@is_file($path) || !is_readable($path)) {
+            if (!static::fileIsAccessible($path)) {
                 throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
             }
 
             // If a MIME type is not specified, try to work it out from the file name
             if ('' === $type) {
@@ -3142,11 +3159,11 @@ class PHPMailer
      * @return string
      */
     protected function encodeFile($path, $encoding = self::ENCODING_BASE64)
     {
         try {
-            if (!static::isPermittedPath($path) || !file_exists($path) || !is_readable($path)) {
+            if (!static::fileIsAccessible($path)) {
                 throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
             }
             $file_buffer = file_get_contents($path);
             if (false === $file_buffer) {
                 throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
@@ -3528,11 +3545,11 @@ class PHPMailer
         $encoding = self::ENCODING_BASE64,
         $type = '',
         $disposition = 'inline'
     ) {
         try {
-            if (!static::isPermittedPath($path) || !@is_file($path) || !is_readable($path)) {
+            if (!static::fileIsAccessible($path)) {
                 throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
             }
 
             // If a MIME type is not specified, try to work it out from the file name
             if ('' === $type) {
diff --git a/test/PHPMailerTest.php b/test/PHPMailerTest.php
index fc4d9a8..ee496a6 100644
--- a/test/PHPMailerTest.php
+++ b/test/PHPMailerTest.php
@@ -1474,10 +1474,13 @@ EOT;
             self::assertTrue(false, $this->Mail->ErrorInfo);
 
             return;
         }
 
+        //Test using non-existent UNC path
+        self::assertFalse($this->Mail->addAttachment('\\\\nowhere\nothing'));
+
         $this->buildBody();
         self::assertTrue($this->Mail->send(), $this->Mail->ErrorInfo);
     }
 
     /**

Vulnerable code:             if (!static::isPermittedPath($lang_file) || !file_exists($lang_file)) {
            if (!static::isPermittedPath($path) || !@is_file($path) || !is_readable($path)) {
            if (!static::isPermittedPath($path) || !file_exists($path) || !is_readable($path)) {
            if (!static::isPermittedPath($path) || !@is_file($path) || !is_readable($path)) {
Fixed code:     /**
     * Check whether a file path is safe, accessible, and readable.
     *
     * @param string $path A relative or absolute path to a file
     *
     * @return bool
     */
    protected static function fileIsAccessible($path)
    {
        $readable = file_exists($path);
        //If not a UNC path (expected to start with \\), check read permission, see #2069
        if (strpos($path, '\\\\') !== 0) {
            $readable = $readable && is_readable($path);
        }
        return static::isPermittedPath($path) && $readable;
    }

            if (!static::fileIsAccessible($lang_file)) {
            if (!static::fileIsAccessible($path)) {
            if (!static::fileIsAccessible($path)) {
            if (!static::fileIsAccessible($path)) {
        //Test using non-existent UNC path
        self::assertFalse($this->Mail->addAttachment('\\\\nowhere\nothing'));