Yii2 login details exposed in error log email

In my ‘log’ component I have set up a yii\log\EmailTarget to email errors. It works fine but I was very concerned today when an error report email exposed my login username and plain text password in the POST report (… ‘LoginForm’ => [ ‘username’ => ‘xxxx’ ‘password’ => ‘xxxxxxxx’]. Can someone please advise me where to look to learn how to prevent this.

I think I’ve already mentioned this somewhere on the forum but I cannot find it so here it goes again.

First of all, if you are sure which data is exactly logged you can exclude it in the logs like mentioned here.

If you are not sure you can use something what we have written for this in our systems. EmailTarget::getContextMessage() is overwritten like:

class EmailTarget extends \yii\log\EmailTarget
{
    protected function getContextMessage()
    {
        $context = \yii\helpers\ArrayHelper::filter($GLOBALS, $this->logVars);
        $result = [];
        foreach ($context as $key => $value) {
            if (is_string($value) && stripos($key, 'password') !== false) {
                $result[] = "\${$key} = '** PASSWORD HIDDEN **'";
            } else {
                $result[] = "\${$key} = " . LogVarDumper::dumpAsString($value);
            }
        }
        return implode("\n\n", $result);
    }
}

And LogVarDumper is introduced:

class LogVarDumper extends \yii\helpers\VarDumper
{
    private static $_objects;
    private static $_output;
    private static $_depth;

    /**
     * Dumps a variable in terms of a string.
     * This method achieves the similar functionality as var_dump and print_r
     * but is more robust when handling complex objects such as Yii controllers.
     * @param mixed $var variable to be dumped
     * @param int $depth maximum depth that the dumper should go into the variable. Defaults to 10.
     * @param bool $highlight whether the result should be syntax-highlighted
     * @return string the string representation of the variable
     */
    public static function dumpAsString($var, $depth = 10, $highlight = false)
    {
        self::$_output = '';
        self::$_objects = [];
        self::$_depth = $depth;
        self::dumpInternal($var, 0);
        if ($highlight) {
            $result = highlight_string("<?php\n" . self::$_output, true);
            self::$_output = preg_replace('/&lt;\\?php<br \\/>/', '', $result, 1);
        }

        return self::$_output;
    }

    /**
     * @param mixed $var variable to be dumped
     * @param int $level depth level
     * @param bool $passwordKey whether password related key was present in previous iteration
     */
    private static function dumpInternal($var, $level, $passwordKey = false)
    {
        switch (\gettype($var)) {
            case 'boolean':
                self::$_output .= $var ? 'true' : 'false';
                break;
            case 'integer':
                self::$_output .= (string) $var;
                break;
            case 'double':
                self::$_output .= (string) $var;
                break;
            case 'string':
                if ($passwordKey) {
                    self::$_output .= "'** PASSWORD HIDDEN **'";
                } else {
                    self::$_output .= "'" . addslashes($var) . "'";
                }
                break;
            case 'resource':
                self::$_output .= '{resource}';
                break;
            case 'NULL':
                self::$_output .= 'null';
                break;
            case 'unknown type':
                self::$_output .= '{unknown}';
                break;
            case 'array':
                if (self::$_depth <= $level) {
                    self::$_output .= '[...]';
                } elseif (empty($var)) {
                    self::$_output .= '[]';
                } else {
                    $keys = array_keys($var);
                    $spaces = str_repeat(' ', $level * 4);
                    self::$_output .= '[';
                    foreach ($keys as $key) {
                        self::$_output .= "\n" . $spaces . '    ';
                        self::dumpInternal($key, 0);
                        self::$_output .= ' => ';
                        self::dumpInternal($var[$key], $level + 1, stripos($key, 'password') !== false);
                    }
                    self::$_output .= "\n" . $spaces . ']';
                }
                break;
            case 'object':
                if (($id = array_search($var, self::$_objects, true)) !== false) {
                    self::$_output .= \get_class($var) . '#' . ($id + 1) . '(...)';
                } elseif (self::$_depth <= $level) {
                    self::$_output .= \get_class($var) . '(...)';
                } else {
                    $id = array_push(self::$_objects, $var);
                    $className = \get_class($var);
                    $spaces = str_repeat(' ', $level * 4);
                    self::$_output .= "$className#$id\n" . $spaces . '(';
                    if ('__PHP_Incomplete_Class' !== \get_class($var) && method_exists($var, '__debugInfo')) {
                        $dumpValues = $var->__debugInfo();
                        if (!\is_array($dumpValues)) {
                            throw new InvalidValueException('__debuginfo() must return an array');
                        }
                    } else {
                        $dumpValues = (array) $var;
                    }
                    foreach ($dumpValues as $key => $value) {
                        $keyDisplay = strtr(trim($key), "\0", ':');
                        self::$_output .= "\n" . $spaces . "    [$keyDisplay] => ";
                        self::dumpInternal($value, $level + 1);
                    }
                    self::$_output .= "\n" . $spaces . ')';
                }
                break;
        }
    }
}

This makes sure that array value is ** PASSWORD HIDDEN **' when its key is password.

Thank you, that was really helpful. Your reply encouraged me to explore the documentation, and after some experimenting adding ‘maskVars’ => [’_POST.LoginForm’,…], has solved the problem (as LoginForm was the array containing username and password). Your more detailed example may be of help in the future. I appreciate how flexible and well documented Yii is, but I am still at the stage of being a bit intimidated by the docs! At least I understood this bit (when I knew where to look). Thanks again.

You don’t even have to go to docs, this is a part of the Guide - https://www.yiiframework.com/doc/guide/2.0/en/runtime-logging (search for “maskVars”)
I assume you’re talking about the “fear” from Api Doc https://www.yiiframework.com/doc/api/2.0