0
0
mirror of https://github.com/nextcloud/server.git synced 2024-11-14 12:26:49 +00:00
nextcloud_server/lib/private/Log/ExceptionSerializer.php
Joas Schilling 8952a4cb77
fix(logger): Fix closure detection when filtering sensitive parameters
Signed-off-by: Joas Schilling <coding@schilljs.com>
2024-11-08 12:59:14 +01:00

308 lines
7.4 KiB
PHP

<?php
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Log;
use OC\Core\Controller\SetupController;
use OC\Http\Client\Client;
use OC\Security\IdentityProof\Key;
use OC\Setup;
use OC\SystemConfig;
use OCA\Encryption\Controller\RecoveryController;
use OCA\Encryption\Controller\SettingsController;
use OCA\Encryption\Crypto\Crypt;
use OCA\Encryption\Crypto\Encryption;
use OCA\Encryption\KeyManager;
use OCA\Encryption\Listeners\UserEventsListener;
use OCA\Encryption\Services\PassphraseService;
use OCA\Encryption\Session;
use OCP\HintException;
class ExceptionSerializer {
public const SENSITIVE_VALUE_PLACEHOLDER = '*** sensitive parameters replaced ***';
public const methodsWithSensitiveParameters = [
// Session/User
'completeLogin',
'login',
'checkPassword',
'checkPasswordNoLogging',
'loginWithPassword',
'updatePrivateKeyPassword',
'validateUserPass',
'loginWithToken',
'{closure}',
'{closure:*',
'createSessionToken',
// Provisioning
'addUser',
// TokenProvider
'getToken',
'isTokenPassword',
'getPassword',
'decryptPassword',
'logClientIn',
'generateToken',
'validateToken',
// TwoFactorAuth
'solveChallenge',
'verifyChallenge',
// ICrypto
'calculateHMAC',
'encrypt',
'decrypt',
// LoginController
'tryLogin',
'confirmPassword',
// LDAP
'bind',
'areCredentialsValid',
'invokeLDAPMethod',
// Encryption
'storeKeyPair',
'setupUser',
'checkSignature',
// files_external: OCA\Files_External\MountConfig
'getBackendStatus',
// files_external: UserStoragesController
'update',
// Preview providers, don't log big data strings
'imagecreatefromstring',
// text: PublicSessionController, SessionController and ApiService
'create',
'close',
'push',
'sync',
'updateSession',
'mention',
'loginSessionUser',
];
public function __construct(
private SystemConfig $systemConfig,
) {
}
protected array $methodsWithSensitiveParametersByClass = [
SetupController::class => [
'run',
'display',
'loadAutoConfig',
],
Setup::class => [
'install'
],
Key::class => [
'__construct'
],
Client::class => [
'request',
'delete',
'deleteAsync',
'get',
'getAsync',
'head',
'headAsync',
'options',
'optionsAsync',
'patch',
'post',
'postAsync',
'put',
'putAsync',
],
\Redis::class => [
'auth'
],
\RedisCluster::class => [
'__construct'
],
Crypt::class => [
'symmetricEncryptFileContent',
'encrypt',
'generatePasswordHash',
'encryptPrivateKey',
'decryptPrivateKey',
'isValidPrivateKey',
'symmetricDecryptFileContent',
'checkSignature',
'createSignature',
'decrypt',
'multiKeyDecrypt',
'multiKeyEncrypt',
],
RecoveryController::class => [
'adminRecovery',
'changeRecoveryPassword'
],
SettingsController::class => [
'updatePrivateKeyPassword',
],
Encryption::class => [
'encrypt',
'decrypt',
],
KeyManager::class => [
'checkRecoveryPassword',
'storeKeyPair',
'setRecoveryKey',
'setPrivateKey',
'setFileKey',
'setAllFileKeys',
],
Session::class => [
'setPrivateKey',
'prepareDecryptAll',
],
\OCA\Encryption\Users\Setup::class => [
'setupUser',
],
UserEventsListener::class => [
'handle',
'onUserCreated',
'onUserLogin',
'onBeforePasswordUpdated',
'onPasswordUpdated',
'onPasswordReset',
],
PassphraseService::class => [
'setPassphraseForUser',
],
];
private function editTrace(array &$sensitiveValues, array $traceLine): array {
if (isset($traceLine['args'])) {
$sensitiveValues = array_merge($sensitiveValues, $traceLine['args']);
}
$traceLine['args'] = [self::SENSITIVE_VALUE_PLACEHOLDER];
return $traceLine;
}
private function filterTrace(array $trace) {
$sensitiveValues = [];
$trace = array_map(function (array $traceLine) use (&$sensitiveValues) {
$className = $traceLine['class'] ?? '';
if ($className && isset($this->methodsWithSensitiveParametersByClass[$className])
&& in_array($traceLine['function'], $this->methodsWithSensitiveParametersByClass[$className], true)) {
return $this->editTrace($sensitiveValues, $traceLine);
}
foreach (self::methodsWithSensitiveParameters as $sensitiveMethod) {
if (str_contains($traceLine['function'], $sensitiveMethod)
|| (str_ends_with($sensitiveMethod, '*')
&& str_starts_with($traceLine['function'], substr($sensitiveMethod, 0, -1)))) {
return $this->editTrace($sensitiveValues, $traceLine);
}
}
return $traceLine;
}, $trace);
return array_map(function (array $traceLine) use ($sensitiveValues) {
if (isset($traceLine['args'])) {
$traceLine['args'] = $this->removeValuesFromArgs($traceLine['args'], $sensitiveValues);
}
return $traceLine;
}, $trace);
}
private function removeValuesFromArgs($args, $values): array {
$workArgs = [];
foreach ($args as $key => $arg) {
if (in_array($arg, $values, true)) {
$arg = self::SENSITIVE_VALUE_PLACEHOLDER;
} elseif (is_array($arg)) {
$arg = $this->removeValuesFromArgs($arg, $values);
}
$workArgs[$key] = $arg;
}
return $workArgs;
}
private function encodeTrace($trace) {
$trace = array_map(function (array $line) {
if (isset($line['args'])) {
$line['args'] = array_map([$this, 'encodeArg'], $line['args']);
}
return $line;
}, $trace);
return $this->filterTrace($trace);
}
private function encodeArg($arg, $nestingLevel = 5) {
if (is_object($arg)) {
if ($nestingLevel === 0) {
return [
'__class__' => get_class($arg),
'__properties__' => 'Encoding skipped as the maximum nesting level was reached',
];
}
$objectInfo = [ '__class__' => get_class($arg) ];
$objectVars = get_object_vars($arg);
return array_map(function ($arg) use ($nestingLevel) {
return $this->encodeArg($arg, $nestingLevel - 1);
}, array_merge($objectInfo, $objectVars));
}
if (is_array($arg)) {
if ($nestingLevel === 0) {
return ['Encoding skipped as the maximum nesting level was reached'];
}
// Only log the first 5 elements of an array unless we are on debug
if ((int)$this->systemConfig->getValue('loglevel', 2) !== 0) {
$elemCount = count($arg);
if ($elemCount > 5) {
$arg = array_slice($arg, 0, 5);
$arg[] = 'And ' . ($elemCount - 5) . ' more entries, set log level to debug to see all entries';
}
}
return array_map(function ($e) use ($nestingLevel) {
return $this->encodeArg($e, $nestingLevel - 1);
}, $arg);
}
return $arg;
}
public function serializeException(\Throwable $exception): array {
$data = [
'Exception' => get_class($exception),
'Message' => $exception->getMessage(),
'Code' => $exception->getCode(),
'Trace' => $this->encodeTrace($exception->getTrace()),
'File' => $exception->getFile(),
'Line' => $exception->getLine(),
];
if ($exception instanceof HintException) {
$data['Hint'] = $exception->getHint();
}
if ($exception->getPrevious()) {
$data['Previous'] = $this->serializeException($exception->getPrevious());
}
return $data;
}
public function enlistSensitiveMethods(string $class, array $methods): void {
if (!isset($this->methodsWithSensitiveParametersByClass[$class])) {
$this->methodsWithSensitiveParametersByClass[$class] = [];
}
$this->methodsWithSensitiveParametersByClass[$class] = array_merge($this->methodsWithSensitiveParametersByClass[$class], $methods);
}
}