代码之家  ›  专栏  ›  技术社区  ›  reformed Ezequiel García

在PHP的debug_backtrace中只隐藏敏感参数的最佳方法是什么?

  •  -1
  • reformed Ezequiel García  · 技术社区  · 4 年前

    请考虑以下代码。如果发生异常,跟踪(将被记录并存储在数据库中)将包括敏感的 password 数据在这种情况下,如何在允许其他非敏感参数的情况下隐藏敏感数据?

    <?php
    $user = 'john';
    $pass = 'secret';
    
    function auth($user, $pass) {
        // authentication logic
    }
    
    function login($user, $pass) {
        throw new Exception('Unexpected error');
    
        // various logic
        auth($user, $pass);
        // various logic
    }
    
    try {
        login($user, $pass);
    } catch (Throwable $e) {
        send_to_log($e->getTrace()); // This reveals the password "secret"
    }
    
    1 回复  |  直到 1 年前
        1
  •  15
  •   hakre    1 年前

    从PHP版本8.2(2022年12月)开始,有一个名为“ Redacting parameters in back traces “。这将对PHP应用程序中的任何堆栈跟踪隐藏该参数。

    以下是中的一个示例 that RFC :

    <?php
     
    function test(
        $foo,
        #[\SensitiveParameter] $bar,
        $baz
    ) {
        throw new \Exception('Error');
    }
     
    test('foo', 'bar', 'baz');
     
    /*
    Fatal error: Uncaught Exception: Error in test.php:8
    Stack trace:
    #0 test.php(11): test('foo', Object(SensitiveParameterValue), 'baz')
    #1 {main}
      thrown in test.php on line 8
    */
    
        2
  •  6
  •   raina77ow    4 年前

    免责声明:我(有点)认为你从来没有真正管道的结果 var_dump 返回给您的用户。很明显,最终用户很少关心引擎的内部,所以向他们显示跟踪路径几乎从来都不是处理服务器错误的好方法。但你是对的;由于各种原因,即使记录这些信息实际上也可能不是一个好主意。

    因此,回答最初的问题:好吧,您可以强制异常日志记录完全丢弃params,或者限制它们的长度:

    注意,PHP 7.4引入了该设置 zend.exception_ignore_args , 这允许从异常中完全删除参数信息 在里面 getTrace() , getTraceAsString()

    背景 zend.exception_string_param_max_len=0 仍然提供了比完全禁用跟踪参数(您仍然知道参数是字符串和非字符串类型)更多的信息。

    不过,这可能会使其他情况的调试变得复杂。在PHP8.0中,通过引入 zend.exception_string_param_max_len config param :

    zend.exception_string_param_max_len是一个新的INI指令,用于设置 字符串化堆栈字符串的参数中的最大字符串长度。

    The idea behind this (quoted above) 除其他外,在记录异常时限制潜在暴露的敏感数据的数量,而不会实际影响调试问题所需的数据。

    但请注意,此设置仅影响 getTraceAsString() results(您应该考虑使用它,而不是使用getTrace的var_dumping结果)。

        3
  •  0
  •   AmigoJack    1 年前

    我的目标不是编辑所有内容,而是

    • 什么可以是密码(键入字符串)或

    • 定义如下:

      • 整体功能(请参阅 $sMatchFunction
      • 数组键的部分匹配(请参阅 $sMatchIndex ).

      两者都是一个正则表达式,正如你在f.e中使用的那样。 preg_match() .作为个人偏好,我使用 # 而不是 / delimiters .

    As per hakre 我还定义了处理时递归次数的限制。属性数组 $aMax 可以进行调整。它的当前值不会影响演示-你必须使用更低的值才能看到用最后一个元素切割的数组,比如:

    [...] => 3 more

    该类有3个入口点来注册所有不同的错误罪魁祸首:

    • ErrorHandler:: err_handler() 用于运行时错误/警告/提示等。。。
    • ErrorHandler:: exc_handler() 对于例外情况
    • ErrorHandler:: die_handler() 用于脚本流产

    他们必须注册(见下文),然后该类在所有三个场合/来源中都是相同的。如果您检查每个处理程序,您将只注意到细微的差异。类从不需要实例。

    <?php
    
    class ErrorHandler {
    
        /*** The core: how to redact only sensitive parts ***
         ****************************************************/
    
        // Redacting values happens either per key in an array element or name in an object property...
        public static $sMatchIndex= '#pass|time#i';
        // ...or per function name. Both are PCRE patterns to match just any text.
        public static $sMatchFunction= '#connect$|login#i';
    
        // In case you think all this exhausts memory you might want to limit the verbosity.
        // If you don't care, set it to a high value, like PHP_INT_MAX.
        public static $aMax= array
        ( 'recursion_count' => 30  // Overall in sum.
        , 'recursion_depth' => 5   // How many levels down.
        , 'array_elements'  => 20  // Avoiding to list whole content of huge arrays.
        , 'class_properties'=> 15  // Class instances may have way too many fields.
        , 'parameters'      => 20  // Function arguments can be excessive, too.
        );
    
    
        // Should every STRING data type be hidden? This is set anew per iterated stack trace function.
        private static $bRedactAllStrings= FALSE;
        // Count limit for aMax anew each time.
        private static $aMaxCount= array();
    
    
        // Handle a variable as per its data type, so an array or an object is recursively checked against 
        // STRINGs, too. Side effect: make STRINGs look like literals. This method should be used on:
        // - every array element value,
        // - every object property value,
        // - optionally on every array element key (to later distinguish numeric indexes from textual).
        public static function as_per_type
        ( $vValue  // Variable of any data type.
        , &$sType  // Recognized data type; is needed later, too.
        , $vKey= ''  // Potential array index to test for password-like name.
        ) {
            $sType= gettype( $vValue );
            switch( $sType ) {
                // Each key and value can have different data types.
                case 'array':
                    return self:: recurse_array( $vValue );
    
                // Each property can have different data types.
                case 'object':
                    return self:: recurse_object( $vValue );
    
                // Either all STRING values should be redacted, or the key has a name hinting for a password.
                case 'string':
                    if( self:: $bRedactAllStrings ) {
                        return '**REDACTED_PER_FUNCTION**';
                    } else
                    if( $vKey&& preg_match( self:: $sMatchIndex, $vKey ) ) {
                        return '**REDACTED_PER_INDEX**'; 
                    } else
                    return "'$vValue'";  // Original text, but as literal.
    
                // BOOLEAN, INTEGER, DOUBLE, RESOURCE, NULL and others: won't have passwords.
                default:
                    return $vValue;
            }
        }
    
    
        // Handle a class instance's properties as per their data types, which can be arrays or objects again.
        public static function recurse_object
        ( $oInput  // Object with any properties.
        ) {
            // Respect recursion depth and overall count.
            if( self:: $aMaxCount['recursion_count']> self:: $aMax['recursion_count']
             || self:: $aMaxCount['recursion_depth']> self:: $aMax['recursion_depth']
              ) {
                return 'O('. count( get_object_vars( $oInput ) ). ')';
            } else self:: $aMaxCount['recursion_count']++;
    
            self:: $aMaxCount['recursion_depth']++;
    
            // Inspect each property.
            $aObj= get_object_vars( $oInput );  // Get all property names as array.
            $aOutput= array();
            $iProperty= 1;
            foreach( $aObj as $iObj=> $vObj ) {
                // Respect maximum element count of array.
                if( $iProperty> self:: $aMax['class_properties'] ) {
                    $aOutput['...']= (count( $aObj )- $iProperty+ 1). ' more';
                    break;
                } else $iProperty++;
    
                $vValue= self:: as_per_type( $oInput-> $iObj, $sType, $iObj );
                $aOutput["$iObj ($sType)"]= $vValue;  // Array key hints at value data type.
            }
    
            self:: $aMaxCount['recursion_depth']--;
            return $aOutput;
        }
    
    
        // Handle all array elements as per their data types, which can be objects or arrays again.
        public static function recurse_array
        ( $aInput  // Array with any elements.
        ) {
            // Respect recursion depth and overall count.
            if( self:: $aMaxCount['recursion_count']> self:: $aMax['recursion_count']
             || self:: $aMaxCount['recursion_depth']> self:: $aMax['recursion_depth']
              ) {
                return 'A('. count( $aInput ). ')';
            } else self:: $aMaxCount['recursion_count']++;
    
            self:: $aMaxCount['recursion_depth']++;
    
            // Inspect each element.
            $aOutput= array();
            $iElement= 1;
            foreach( $aInput as $iKey=> $vValue ) {
                // Respect maximum element count of array.
                if( $iElement> self:: $aMax['array_elements'] ) {
                    $aOutput['...']= (count( $aInput )- $iElement+ 1). ' more';
                    break;
                } else $iElement++;
    
                $sKey= self:: as_per_type( $iKey, $sTypeKey );  // Element keys need no redaction...
                $sValue= self:: as_per_type( $vValue, $sTypeValue, $iKey );  // ...but values do.
    
                // Objects are converted to arrays by us, loosing the information of which class they were.     
                // So we append the class name to the type hint in the array element key.
                if( $sTypeValue== 'object' ) $sTypeValue.= ' '. get_class( $vValue );
    
                $aOutput["$sKey ($sTypeValue)"]= $sValue;  // Array key hints at value data type.
            }
    
            self:: $aMaxCount['recursion_depth']--;
            return $aOutput;
        }
    
    
        // Parse the stack trace to redact potentially sensitive texts.
        public static function redact_backtrace
        ( $aTrace  // Stack trace array to be parsed.
        ) {
            // Reset on each new error handling, as this is the entry of every further processing.
            self:: $aMaxCount= array
            ( 'recursion_count'=> 0
            , 'recursion_depth'=> 1
            );
    
            foreach( $aTrace as $iFunc=> $aFunc ) {
                // Yet this is no sensitive function being called.
                self:: $bRedactAllStrings= FALSE;
    
                // If this is a class instance we only need to redact by property name.
                if( isset( $aFunc['object'] ) ) {
                    $aTrace[$iFunc]['object']= self:: recurse_object( $aTrace[$iFunc]['object'] );
                }
    
                // Should the function's name match we'll recursively redact ANY string.
                if( isset( $aFunc['function'] ) ) {
                    self:: $bRedactAllStrings= preg_match( self:: $sMatchFunction, $aFunc['function'] );
                }
    
                // Now parse all parameters to potentially redact chosen ones.
                if( isset( $aFunc['args'] ) ) {
                    // Respect amount of parameters.
                    $iRemoved= 0;
                    while( count( $aTrace[$iFunc]['args'] )> self:: $aMax['parameters'] ) {
                        array_pop( $aTrace[$iFunc]['args'] );
                        $iRemoved++;
                    }
    
                    $aTrace[$iFunc]['args']= self:: recurse_array( $aTrace[$iFunc]['args'] );
    
                    // Inform about too many parameters.
                    if( $iRemoved ) $aTrace[$iFunc]['args']['...']= $iRemoved. ' more';
                }
            }
    
            return $aTrace;
        }
    
    
    
        /*** Functional example: seeing the redacted data ***
         ****************************************************/
    
        // Write log messages to wherever we want to.
        private static $bHeadersSent= FALSE;
        public static function err_log
        ( $aLog  // Array to be saved.
        ) {
            if( !self:: $bHeadersSent ) {
                header( 'content-type: text/plain' );  // Don't let browser interpret output as HTML, preserve spaces.
                self:: $bHeadersSent= TRUE;  // Only send header once.
            }
    
            print_r( $aLog );  // Imagine this being our log file.
        }
    
    
    
        /*** Demo: actually handling errors to get stack traces ***
         **********************************************************/
    
        // Handler for uncaught errors.
        public static function err_handler
        ( $iError  // See https://www.php.net/manual/en/errorfunc.constants.php
        , $sText  // Error message.
        , $sFile  // PHP file which was parsed.
        , $iLine  // Line of error in PHP file.
        ) {
            // First one is this function, and we won't need this ever
            $aTrace= debug_backtrace();
            unset( $aTrace[0] );
    
            self:: err_log
            ( array
                ( 'where'   => 'Error handler'
                , 'file'    => $sFile
                , 'line'    => $iLine
                , 'code'    => $iError
                , 'msg'     => $sText
                , 'trace'   => self:: redact_backtrace( $aTrace )
                )
            );
        }
    
        // Handler for uncaught exceptions.
        public static function exc_handler
        ( $e  // Exception
        ) {
            self:: err_log
            ( array
                ( 'where'   => 'Exception handler'
                , 'file'    => $e-> getFile()
                , 'line'    => $e-> getLine()
                , 'code'    => $e-> getCode()
                , 'msg'     => $e-> getMessage()
                , 'trace'   => self:: redact_backtrace( $e-> getTrace() )
                , 'class'   => get_class( $e )
                )
            );
        }
    
        // Handler for potentially fatal errors.
        public static function die_handler() {
            // No error occurred? Nothing to inspect.
            $aErr= error_get_last();
            if( !count( $aErr ) ) return;
    
            // First one is this function, and we won't need this ever
            $aTrace= debug_backtrace();
            unset( $aTrace[0] );
    
            self:: err_log
            ( array
                ( 'where'   => 'Shutdown handler'
                , 'file'    => $aErr['file']
                , 'line'    => $aErr['line']
                , 'code'    => $aErr['type']
                , 'msg'     => $aErr['message']
                , 'trace'   => self:: redact_backtrace( $aTrace )
                )
            );
        }
    }
    
    // For register_shutdown_function() a stack trace is not available.
    set_error_handler           ( array( 'ErrorHandler', 'err_handler' ), E_ALL );
    set_exception_handler       ( array( 'ErrorHandler', 'exc_handler' ) );
    register_shutdown_function  ( array( 'ErrorHandler', 'die_handler' ) );
    
    
    
    /*** Demo: creating errors ***
     *****************************/
    
    class Example {
        public $iNumber     = 12345;  // Integers won't be redacted.
        public $sPassword   = 'secret';  // The property name should hint at a password.
        public $sInfo       = 'a password?';  // No chance to identify this as password.
    
        public function login( $sUser, $sPass ) {
            echo( array() );  // Notice: Array to string conversion.
        }
    
        public function test( $a, $b ) {
            $this-> login( 'username', 'password' );  // Deeper nesting, recognition by function name.
    
            unset( $a['obj'] );  // Seeing the object once is enough for demonstration purposes.
    
            1/ 0;  // Error: Division by zero.
            1+ $x;  // Error: Undefined variable.
            throw new Exception( 'TestException' );  // Unhandled exception.
        }
    }
    
    
    // Building a rather complex parameter, using as many data types as possible.
    $aFirst= array
    ( 'string'  => 'Text'
    , 'int'     => 42
    , 'float'   => 3.1415
    , 'bool'    => TRUE
    , 'array'   => array
        ( 'key'         => 'value'
        , 'db_password' => 'adminadmin'  // Array in array: should be redacted as per key text.
        )
    , 'obj'     => new DateTime  // So we get an actual class instance.
    , 'pass'    => '12345'  // Should be redacted as per key text.
    , 110       => 'ordinal index'
    );
    
    // Simple parameter: array with ordinal indexes only.
    $aSecond= array
    ( 'no index identifying a password'
    , 'username'
    );
    
    
    // Outcome:
    // WHERE                           | REDACTION
    // --------------------------------|----------
    // Example-> login()               |
    // - $oTest-> sPassword            | Index
    // - $sUser  (1st function param)  | Function
    // - $sPass  (2nd function param)  | Function
    // Example-> test()                |
    // - $oTest-> sPassword            | Index
    // - $a['array']['db_password']    | Index
    // - $a['obj']-> timezone          | Index
    // - $a['pass']                    | Index
    $oTest= new Example;
    $oTest-> test( $aFirst, $aSecond );
    

    这比 reformed's answer 目标受众是对PHP有信心或想投入时间来理解它的人。我试图通过解释尽可能多地发表评论。

        4
  •  0
  •   reformed Ezequiel García    1 年前

    最后,我在处理文件/数据库日志记录的代码中添加了逻辑,以清除跟踪中显示的特定函数的参数。这里有一个合适的<PHP 8.2解决方案:

    <?php
    function send_to_log(Throwable $e) {
        $noArgs = [
            'login' => true,
            'auth' => true,
            // ...
        ];
    
        $trace = $e->getTrace();
        foreach ($trace as &$err) {
            if (isset($noArgs[$err['function'] ?? ''])) {
                $cnt = count($err['args'] ?? []);
                if ($cnt > 0) {
                    $err['args'] = array_fill(0, $cnt, 'REDACTED');
                }
            }
        }
        unset($err);
    
        var_dump($trace); /* This now shows "REDACTED" for all arguments
        to functions specified in the $noArgs array */
    
        // logging logic
    }