diff --git a/_config/backtrace.yml b/_config/backtrace.yml new file mode 100644 index 000000000..dd90eabbe --- /dev/null +++ b/_config/backtrace.yml @@ -0,0 +1,43 @@ +--- +Name: framework-backtrace +--- +SilverStripe\Dev\Backtrace: + ignore_function_args: + - 'mssql_connect' + - 'mysql_connect' + - 'pg_connect' + - ['mysqli', 'mysqli'] + - ['mysqli', 'real_connect'] + - ['mysqli', 'select_db'] + - ['PDO', '__construct'] + - ['SilverStripe\Control\Middleware\ConfirmationMiddleware\GetParameter', buildConfirmationItem] + - ['SilverStripe\Control\Middleware\ConfirmationMiddleware\Url', buildConfirmationItem] + - ['SilverStripe\Control\Middleware\ConfirmationMiddleware\UrlPathStartswith', buildConfirmationItem] + - ['SilverStripe\Core\Startup\AbstractConfirmationToken', 'checkToken'] + - ['SilverStripe\Core\Startup\AbstractConfirmationToken', 'pathForToken'] + - ['SilverStripe\Core\Startup\AbstractConfirmationToken', 'prepare_tokens'] + - ['SilverStripe\ORM\DB', 'connect'] + - ['SilverStripe\ORM\DB', 'createDatabase'] + - ['SilverStripe\Security\Confirmation\Item', '__construct'] + - ['SilverStripe\Security\DefaultAdminService', 'isDefaultAdminCredentials'] + - ['SilverStripe\Security\DefaultAdminService', 'setDefaultAdmin'] + - ['SilverStripe\Security\Member', 'changePassword'] + - ['SilverStripe\Security\MemberAuthenticator\ChangePasswordHandler', 'setSessionToken'] + - ['SilverStripe\Security\MemberAuthenticator\CookieAuthenticationHandler', 'setTokenCookieName'] + - ['SilverStripe\Security\MemberAuthenticator\CookieAuthenticationHandler', 'setTokenCookieSecure'] + - ['SilverStripe\Security\MemberAuthenticator\LostPasswordHandler', 'sendEmail'] + - ['SilverStripe\Security\PasswordEncryptor', 'check'] + - ['SilverStripe\Security\PasswordEncryptor', 'encrypt'] + - ['SilverStripe\Security\PasswordEncryptor', 'salt'] + - ['SilverStripe\Security\PasswordEncryptor_Blowfish', 'encryptA'] + - ['SilverStripe\Security\PasswordEncryptor_Blowfish', 'encryptX'] + - ['SilverStripe\Security\PasswordEncryptor_Blowfish', 'encryptY'] + - ['SilverStripe\Security\PasswordValidator', 'validate'] + - ['SilverStripe\Security\RememberLoginHash', 'setToken'] + - ['SilverStripe\Security\Security', 'check_default_admin'] + - ['SilverStripe\Security\Security', 'encrypt_password'] + - ['SilverStripe\Security\Security', 'setDefaultAdmin'] + - ['*', 'checkPassword'] + - ['*', 'onAfterChangePassword'] + - ['*', 'onBeforeChangePassword'] + - ['*', 'updateValidatePassword'] diff --git a/src/Dev/Backtrace.php b/src/Dev/Backtrace.php index 9139534f4..deae15921 100644 --- a/src/Dev/Backtrace.php +++ b/src/Dev/Backtrace.php @@ -13,42 +13,14 @@ class Backtrace use Configurable; /** - * @var array Replaces all arguments with a '' string, + * Replaces all arguments with a '' string, * mostly for security reasons. Use string values for global functions, * and array notation for class methods. * PHP's debug_backtrace() doesn't allow to inspect the argument names, * so all arguments of the provided functions will be filtered out. + * @var array */ - private static $ignore_function_args = [ - 'mysql_connect', - 'mssql_connect', - 'pg_connect', - ['PDO', '__construct'], - ['mysqli', 'mysqli'], - ['mysqli', 'select_db'], - ['mysqli', 'real_connect'], - ['SilverStripe\\ORM\\DB', 'connect'], - ['SilverStripe\\Security\\Security', 'check_default_admin'], - ['SilverStripe\\Security\\Security', 'encrypt_password'], - ['SilverStripe\\Security\\Security', 'setDefaultAdmin'], - ['SilverStripe\\ORM\\DB', 'createDatabase'], - ['SilverStripe\\Security\\Member', 'checkPassword'], - ['SilverStripe\\Security\\Member', 'changePassword'], - ['SilverStripe\\Security\\MemberAuthenticator\\MemberAuthenticator', 'checkPassword'], - ['SilverStripe\\Security\\MemberPassword', 'checkPassword'], - ['SilverStripe\\Security\\PasswordValidator', 'validate'], - ['SilverStripe\\Security\\PasswordEncryptor_PHPHash', 'encrypt'], - ['SilverStripe\\Security\\PasswordEncryptor_PHPHash', 'salt'], - ['SilverStripe\\Security\\PasswordEncryptor_LegacyPHPHash', 'encrypt'], - ['SilverStripe\\Security\\PasswordEncryptor_LegacyPHPHash', 'salt'], - ['SilverStripe\\Security\\PasswordEncryptor_MySQLPassword', 'encrypt'], - ['SilverStripe\\Security\\PasswordEncryptor_MySQLPassword', 'salt'], - ['SilverStripe\\Security\\PasswordEncryptor_MySQLOldPassword', 'encrypt'], - ['SilverStripe\\Security\\PasswordEncryptor_MySQLOldPassword', 'salt'], - ['SilverStripe\\Security\\PasswordEncryptor_Blowfish', 'encrypt'], - ['SilverStripe\\Security\\PasswordEncryptor_Blowfish', 'salt'], - ['*', 'updateValidatePassword'], - ]; + private static $ignore_function_args = []; /** * Return debug_backtrace() results with functions filtered @@ -107,22 +79,23 @@ class Backtrace // Filter out arguments foreach ($bt as $i => $frame) { $match = false; - if (!empty($bt[$i]['class'])) { + if (!empty($frame['class'])) { foreach ($ignoredArgs as $fnSpec) { - if (is_array($fnSpec) && - ('*' == $fnSpec[0] || $bt[$i]['class'] == $fnSpec[0]) && - $bt[$i]['function'] == $fnSpec[1] + if (is_array($fnSpec) + && self::matchesFilterableClass($frame['class'], $fnSpec[0]) + && $frame['function'] == $fnSpec[1] ) { $match = true; + break; } } } else { - if (in_array($bt[$i]['function'], $ignoredArgs ?? [])) { + if (in_array($frame['function'], $ignoredArgs ?? [])) { $match = true; } } if ($match) { - foreach ($bt[$i]['args'] as $j => $arg) { + foreach ($frame['args'] as $j => $arg) { $bt[$i]['args'][$j] = ''; } } @@ -229,4 +202,13 @@ class Backtrace } return $result; } + + /** + * Checks if the filterable class is wildcard, of if the class name is the filterable class, or a subclass of it, + * or implements it. + */ + private static function matchesFilterableClass(string $className, string $filterableClass): bool + { + return $filterableClass === '*' || $className === $filterableClass || is_subclass_of($className, $filterableClass); + } } diff --git a/tests/php/Dev/BacktraceTest.php b/tests/php/Dev/BacktraceTest.php index 89f3c3285..de44410d1 100644 --- a/tests/php/Dev/BacktraceTest.php +++ b/tests/php/Dev/BacktraceTest.php @@ -2,8 +2,13 @@ namespace SilverStripe\Dev\Tests; +use ReflectionMethod; +use SilverStripe\Core\BaseKernel; +use SilverStripe\Core\CoreKernel; +use SilverStripe\Core\Kernel; use SilverStripe\Dev\Backtrace; use SilverStripe\Dev\SapphireTest; +use SilverStripe\ORM\DataObject; class BacktraceTest extends SapphireTest { @@ -109,4 +114,56 @@ class BacktraceTest extends SapphireTest $this->assertEquals('', $filtered[1]['args']['password']); $this->assertEquals('myval', $filtered[2]['args']['myarg']); } + + public function matchesFilterableClassProvider(): array + { + return [ + [ + 'anything', + '*', + true, + 'Wildcard counts as a match', + ], + [ + DataObject::class, + BaseKernel::class, + false, + 'No match', + ], + [ + DataObject::class, + DataObject::class, + true, + 'Exact match', + ], + [ + CoreKernel::class, + BaseKernel::class, + true, + 'Subclass counts as a match', + ], + [ + BaseKernel::class, + CoreKernel::class, + false, + 'Superclass does not count as a match', + ], + [ + CoreKernel::class, + Kernel::class, + true, + 'Implements interface counts as a match', + ], + ]; + } + + /** + * @dataProvider matchesFilterableClassProvider + */ + public function testMatchesFilterableClass(string $className, string $filterableClass, bool $expected, string $message): void + { + $reflectionMethod = new ReflectionMethod(Backtrace::class . '::matchesFilterableClass'); + $reflectionMethod->setAccessible(true); + $this->assertSame($expected, $reflectionMethod->invoke(null, $className, $filterableClass), $message); + } }