My goal is to not redact everything, but only
As per hakre I also defined limits on how much to recurse while processing. The property array $aMax
can be adjusted. Its current values won't affect the demo - you'd have to use much lower values to then see arrays cut with a final element like:
[...] => 3 more
The class has 3 entry points to register all different culprits of errors:
ErrorHandler:: err_handler()
for runtime errors/warnings/hints/etc...
ErrorHandler:: exc_handler()
for exceptions
ErrorHandler:: die_handler()
for script abortions
They have to be registered (see below) and then the class works the same for all 3 occasions/sources. If you inspect each handler you will notice only minor differences. The class never needs an instance.
<?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 );
This is much more code/complex than reformed's answer and the target audience are people being confident with PHP or wanting to invest time to understand it. I tried to comment as much as possible with explanations.