CVE-2018-6010: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: In Yii Framework 2.x before 2.0.14, remote attackers could obtain potentially sensitive information from exception messages, or exploit reflected XSS on the error handler page in non-debug mode. Related to base/ErrorHandler.php, log/Dispatcher.php, and views/errorHandler/exception.php.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 7.5
Published: 2018-01-22T22:29Z
Repository: https://github.com/yiisoft/yii2
Language: PHP
Files changed: framework/CHANGELOG.md, framework/base/ErrorHandler.php, framework/log/Dispatcher.php, framework/views/errorHandler/exception.php, tests/framework/log/DispatcherTest.php
Commit date: 2018-01-22 11:41:24 +0300
Commit message: Fixes #14711: Fixed `yii\web\ErrorHandler` displaying exception message in non-debug mode


Diff: diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md
index 5444f47e0b..66b28e5fe7 100644
--- a/framework/CHANGELOG.md
+++ b/framework/CHANGELOG.md
@@ -2,10 +2,11 @@ Yii Framework 2 Change Log
 ==========================
 
 2.0.14 under development
 ------------------------
 
+- Bug #14711: Fixed `yii\web\ErrorHandler` displaying exception message in non-debug mode (samdark)
 - Enh #13814: MySQL unique index names can now contain spaces (df2)
 - Bug #15300: Fixed "Cannot read property 'style' of undefined" error at the error screen (vitorarantes)
 - Enh #15426: Added abilitiy to create and drop database views (igravity, vladis84)
 - Enh #10186: Use native `hash_equals` in `yii\base\Security::compareString()` if available, throw exception if non-strings are compared (aotd1, samdark)
 - Bug #15122: Fixed `yii\db\Command::getRawSql()` to properly replace expressions (hiscaler, samdark)
diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php
index 1ec96087ed..a62529199f 100644
--- a/framework/base/ErrorHandler.php
+++ b/framework/base/ErrorHandler.php
@@ -322,25 +322,39 @@ abstract class ErrorHandler extends Component
      * @param \Exception|\Error $exception the exception being converted
      * @return string the string representation of the exception.
      */
     public static function convertExceptionToString($exception)
     {
-        if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) {
-            $message = "{$exception->getName()}: {$exception->getMessage()}";
-        } elseif (YII_DEBUG) {
-            if ($exception instanceof Exception) {
-                $message = "Exception ({$exception->getName()})";
-            } elseif ($exception instanceof ErrorException) {
-                $message = "{$exception->getName()}";
-            } else {
-                $message = 'Exception';
-            }
-            $message .= " '" . get_class($exception) . "' with message '{$exception->getMessage()}' \n\nin "
-                . $exception->getFile() . ':' . $exception->getLine() . "\n\n"
-                . "Stack trace:\n" . $exception->getTraceAsString();
+        if ($exception instanceof UserException) {
+            return "{$exception->getName()}: {$exception->getMessage()}";
+        }
+
+        if (YII_DEBUG) {
+            return static::convertExceptionToVerboseString($exception);
+        }
+
+        return 'An internal server error occurred.';
+    }
+
+    /**
+     * Converts an exception into a string that has verbose information about the exception and its trace.
+     * @param \Exception|\Error $exception the exception being converted
+     * @return string the string representation of the exception.
+     *
+     * @since 2.0.14
+     */
+    public static function convertExceptionToVerboseString($exception)
+    {
+        if ($exception instanceof Exception) {
+            $message = "Exception ({$exception->getName()})";
+        } elseif ($exception instanceof ErrorException) {
+            $message = "{$exception->getName()}";
         } else {
-            $message = 'Error: ' . $exception->getMessage();
+            $message = 'Exception';
         }
+        $message .= " '" . get_class($exception) . "' with message '{$exception->getMessage()}' \n\nin "
+            . $exception->getFile() . ':' . $exception->getLine() . "\n\n"
+            . "Stack trace:\n" . $exception->getTraceAsString();
 
         return $message;
     }
 }
diff --git a/framework/log/Dispatcher.php b/framework/log/Dispatcher.php
index 80ad9b590e..7e5c72aa1b 100644
--- a/framework/log/Dispatcher.php
+++ b/framework/log/Dispatcher.php
@@ -188,11 +188,11 @@ class Dispatcher extends Component
                 try {
                     $target->collect($messages, $final);
                 } catch (\Exception $e) {
                     $target->enabled = false;
                     $targetErrors[] = [
-                        'Unable to send log via ' . get_class($target) . ': ' . ErrorHandler::convertExceptionToString($e),
+                        'Unable to send log via ' . get_class($target) . ': ' . ErrorHandler::convertExceptionToVerboseString($e),
                         Logger::LEVEL_WARNING,
                         __METHOD__,
                         microtime(true),
                         [],
                     ];
diff --git a/framework/views/errorHandler/exception.php b/framework/views/errorHandler/exception.php
index 42916d5fba..0b5b26e93d 100644
--- a/framework/views/errorHandler/exception.php
+++ b/framework/views/errorHandler/exception.php
@@ -15,11 +15,10 @@
     <title><?php
         $name = $handler->getExceptionName($exception);
         if ($exception instanceof \yii\web\HttpException) {
             echo (int) $exception->statusCode . ' ' . $handler->htmlEncode($name);
         } else {
-            $name = $handler->getExceptionName($exception);
             if ($name !== null) {
                 echo $handler->htmlEncode($name . ' – ' . get_class($exception));
             } else {
                 echo $handler->htmlEncode(get_class($exception));
             }
diff --git a/tests/framework/log/DispatcherTest.php b/tests/framework/log/DispatcherTest.php
index b2944b637c..ef1401896e 100644
--- a/tests/framework/log/DispatcherTest.php
+++ b/tests/framework/log/DispatcherTest.php
@@ -201,17 +201,37 @@ namespace yiiunit\framework\log {
             $target1->expects($this->exactly(2))
                 ->method('collect')
                 ->withConsecutive(
                     [$this->equalTo('messages'), $this->equalTo(true)],
                     [
-                        [[
-                            'Unable to send log via ' . get_class($target1) . ': Exception: some error',
-                            Logger::LEVEL_WARNING,
-                            'yii\log\Dispatcher::dispatch',
-                            'time data',
-                            [],
-                        ]],
+                        $this->callback(function($arg) use ($target1) {
+                            if (!isset($arg[0][0], $arg[0][1], $arg[0][2], $arg[0][3])) {
+                                return false;
+                            }
+
+                            if (strpos($arg[0][0], 'Unable to send log via ' . get_class($target1) . ': Exception (Exception) \'yii\base\UserException\' with message \'some error\'') !== 0) {
+                                return false;
+                            }
+
+                            if ($arg[0][1] !== Logger::LEVEL_WARNING) {
+                                return false;
+                            }
+
+                            if ($arg[0][2] !== 'yii\log\Dispatcher::dispatch') {
+                                return false;
+                            }
+
+                            if ($arg[0][3] !== 'time data') {
+                                return false;
+                            }
+
+                            if ($arg[0][4] !== []) {
+                                return false;
+                            }
+
+                            return true;
+                        }),
                         true,
                     ]
                 );
 
             $target2->expects($this->once())

Vulnerable code:         if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) {
            $message = "{$exception->getName()}: {$exception->getMessage()}";
        } elseif (YII_DEBUG) {
            if ($exception instanceof Exception) {
                $message = "Exception ({$exception->getName()})";
            } elseif ($exception instanceof ErrorException) {
                $message = "{$exception->getName()}";
            } else {
                $message = 'Exception';
            }
            $message .= " '" . get_class($exception) . "' with message '{$exception->getMessage()}' \n\nin "
                . $exception->getFile() . ':' . $exception->getLine() . "\n\n"
                . "Stack trace:\n" . $exception->getTraceAsString();
            $message = 'Error: ' . $exception->getMessage();
                        'Unable to send log via ' . get_class($target) . ': ' . ErrorHandler::convertExceptionToString($e),
            $name = $handler->getExceptionName($exception);
                        [[
                            'Unable to send log via ' . get_class($target1) . ': Exception: some error',
                            Logger::LEVEL_WARNING,
                            'yii\log\Dispatcher::dispatch',
                            'time data',
                            [],
                        ]],
Fixed code: - Bug #14711: Fixed `yii\web\ErrorHandler` displaying exception message in non-debug mode (samdark)
        if ($exception instanceof UserException) {
            return "{$exception->getName()}: {$exception->getMessage()}";
        }

        if (YII_DEBUG) {
            return static::convertExceptionToVerboseString($exception);
        }

        return 'An internal server error occurred.';
    }

    /**
     * Converts an exception into a string that has verbose information about the exception and its trace.
     * @param \Exception|\Error $exception the exception being converted
     * @return string the string representation of the exception.
     *
     * @since 2.0.14
     */
    public static function convertExceptionToVerboseString($exception)
    {
        if ($exception instanceof Exception) {
            $message = "Exception ({$exception->getName()})";
        } elseif ($exception instanceof ErrorException) {
            $message = "{$exception->getName()}";
            $message = 'Exception';
        $message .= " '" . get_class($exception) . "' with message '{$exception->getMessage()}' \n\nin "
            . $exception->getFile() . ':' . $exception->getLine() . "\n\n"
            . "Stack trace:\n" . $exception->getTraceAsString();
                        'Unable to send log via ' . get_class($target) . ': ' . ErrorHandler::convertExceptionToVerboseString($e),
                        $this->callback(function($arg) use ($target1) {
                            if (!isset($arg[0][0], $arg[0][1], $arg[0][2], $arg[0][3])) {
                                return false;
                            }

                            if (strpos($arg[0][0], 'Unable to send log via ' . get_class($target1) . ': Exception (Exception) \'yii\base\UserException\' with message \'some error\'') !== 0) {
                                return false;
                            }

                            if ($arg[0][1] !== Logger::LEVEL_WARNING) {
                                return false;
                            }

                            if ($arg[0][2] !== 'yii\log\Dispatcher::dispatch') {
                                return false;
                            }

                            if ($arg[0][3] !== 'time data') {
                                return false;
                            }

                            if ($arg[0][4] !== []) {
                                return false;
                            }

                            return true;
                        }),