ExceptionSerializer.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. namespace OC\Log;
  7. use OC\Core\Controller\SetupController;
  8. use OC\Security\IdentityProof\Key;
  9. use OC\Setup;
  10. use OC\SystemConfig;
  11. use OCA\Encryption\Controller\RecoveryController;
  12. use OCA\Encryption\Controller\SettingsController;
  13. use OCA\Encryption\Crypto\Crypt;
  14. use OCA\Encryption\Crypto\Encryption;
  15. use OCA\Encryption\Hooks\UserHooks;
  16. use OCA\Encryption\KeyManager;
  17. use OCA\Encryption\Session;
  18. use OCP\HintException;
  19. class ExceptionSerializer {
  20. public const SENSITIVE_VALUE_PLACEHOLDER = '*** sensitive parameters replaced ***';
  21. public const methodsWithSensitiveParameters = [
  22. // Session/User
  23. 'completeLogin',
  24. 'login',
  25. 'checkPassword',
  26. 'checkPasswordNoLogging',
  27. 'loginWithPassword',
  28. 'updatePrivateKeyPassword',
  29. 'validateUserPass',
  30. 'loginWithToken',
  31. '{closure}',
  32. 'createSessionToken',
  33. // Provisioning
  34. 'addUser',
  35. // TokenProvider
  36. 'getToken',
  37. 'isTokenPassword',
  38. 'getPassword',
  39. 'decryptPassword',
  40. 'logClientIn',
  41. 'generateToken',
  42. 'validateToken',
  43. // TwoFactorAuth
  44. 'solveChallenge',
  45. 'verifyChallenge',
  46. // ICrypto
  47. 'calculateHMAC',
  48. 'encrypt',
  49. 'decrypt',
  50. // LoginController
  51. 'tryLogin',
  52. 'confirmPassword',
  53. // LDAP
  54. 'bind',
  55. 'areCredentialsValid',
  56. 'invokeLDAPMethod',
  57. // Encryption
  58. 'storeKeyPair',
  59. 'setupUser',
  60. 'checkSignature',
  61. // files_external: OCA\Files_External\MountConfig
  62. 'getBackendStatus',
  63. // files_external: UserStoragesController
  64. 'update',
  65. // Preview providers, don't log big data strings
  66. 'imagecreatefromstring',
  67. // text: PublicSessionController, SessionController and ApiService
  68. 'create',
  69. 'close',
  70. 'push',
  71. 'sync',
  72. 'updateSession',
  73. 'mention',
  74. 'loginSessionUser',
  75. ];
  76. public function __construct(
  77. private SystemConfig $systemConfig,
  78. ) {
  79. }
  80. protected array $methodsWithSensitiveParametersByClass = [
  81. SetupController::class => [
  82. 'run',
  83. 'display',
  84. 'loadAutoConfig',
  85. ],
  86. Setup::class => [
  87. 'install'
  88. ],
  89. Key::class => [
  90. '__construct'
  91. ],
  92. \Redis::class => [
  93. 'auth'
  94. ],
  95. \RedisCluster::class => [
  96. '__construct'
  97. ],
  98. Crypt::class => [
  99. 'symmetricEncryptFileContent',
  100. 'encrypt',
  101. 'generatePasswordHash',
  102. 'encryptPrivateKey',
  103. 'decryptPrivateKey',
  104. 'isValidPrivateKey',
  105. 'symmetricDecryptFileContent',
  106. 'checkSignature',
  107. 'createSignature',
  108. 'decrypt',
  109. 'multiKeyDecrypt',
  110. 'multiKeyEncrypt',
  111. ],
  112. RecoveryController::class => [
  113. 'adminRecovery',
  114. 'changeRecoveryPassword'
  115. ],
  116. SettingsController::class => [
  117. 'updatePrivateKeyPassword',
  118. ],
  119. Encryption::class => [
  120. 'encrypt',
  121. 'decrypt',
  122. ],
  123. KeyManager::class => [
  124. 'checkRecoveryPassword',
  125. 'storeKeyPair',
  126. 'setRecoveryKey',
  127. 'setPrivateKey',
  128. 'setFileKey',
  129. 'setAllFileKeys',
  130. ],
  131. Session::class => [
  132. 'setPrivateKey',
  133. 'prepareDecryptAll',
  134. ],
  135. \OCA\Encryption\Users\Setup::class => [
  136. 'setupUser',
  137. ],
  138. UserHooks::class => [
  139. 'login',
  140. 'postCreateUser',
  141. 'postDeleteUser',
  142. 'prePasswordReset',
  143. 'postPasswordReset',
  144. 'preSetPassphrase',
  145. 'setPassphrase',
  146. ],
  147. ];
  148. private function editTrace(array &$sensitiveValues, array $traceLine): array {
  149. if (isset($traceLine['args'])) {
  150. $sensitiveValues = array_merge($sensitiveValues, $traceLine['args']);
  151. }
  152. $traceLine['args'] = [self::SENSITIVE_VALUE_PLACEHOLDER];
  153. return $traceLine;
  154. }
  155. private function filterTrace(array $trace) {
  156. $sensitiveValues = [];
  157. $trace = array_map(function (array $traceLine) use (&$sensitiveValues) {
  158. $className = $traceLine['class'] ?? '';
  159. if ($className && isset($this->methodsWithSensitiveParametersByClass[$className])
  160. && in_array($traceLine['function'], $this->methodsWithSensitiveParametersByClass[$className], true)) {
  161. return $this->editTrace($sensitiveValues, $traceLine);
  162. }
  163. foreach (self::methodsWithSensitiveParameters as $sensitiveMethod) {
  164. if (str_contains($traceLine['function'], $sensitiveMethod)) {
  165. return $this->editTrace($sensitiveValues, $traceLine);
  166. }
  167. }
  168. return $traceLine;
  169. }, $trace);
  170. return array_map(function (array $traceLine) use ($sensitiveValues) {
  171. if (isset($traceLine['args'])) {
  172. $traceLine['args'] = $this->removeValuesFromArgs($traceLine['args'], $sensitiveValues);
  173. }
  174. return $traceLine;
  175. }, $trace);
  176. }
  177. private function removeValuesFromArgs($args, $values): array {
  178. $workArgs = [];
  179. foreach ($args as $arg) {
  180. if (in_array($arg, $values, true)) {
  181. $arg = self::SENSITIVE_VALUE_PLACEHOLDER;
  182. } elseif (is_array($arg)) {
  183. $arg = $this->removeValuesFromArgs($arg, $values);
  184. }
  185. $workArgs[] = $arg;
  186. }
  187. return $workArgs;
  188. }
  189. private function encodeTrace($trace) {
  190. $trace = array_map(function (array $line) {
  191. if (isset($line['args'])) {
  192. $line['args'] = array_map([$this, 'encodeArg'], $line['args']);
  193. }
  194. return $line;
  195. }, $trace);
  196. return $this->filterTrace($trace);
  197. }
  198. private function encodeArg($arg, $nestingLevel = 5) {
  199. if (is_object($arg)) {
  200. if ($nestingLevel === 0) {
  201. return [
  202. '__class__' => get_class($arg),
  203. '__properties__' => 'Encoding skipped as the maximum nesting level was reached',
  204. ];
  205. }
  206. $objectInfo = [ '__class__' => get_class($arg) ];
  207. $objectVars = get_object_vars($arg);
  208. return array_map(function ($arg) use ($nestingLevel) {
  209. return $this->encodeArg($arg, $nestingLevel - 1);
  210. }, array_merge($objectInfo, $objectVars));
  211. }
  212. if (is_array($arg)) {
  213. if ($nestingLevel === 0) {
  214. return ['Encoding skipped as the maximum nesting level was reached'];
  215. }
  216. // Only log the first 5 elements of an array unless we are on debug
  217. if ((int)$this->systemConfig->getValue('loglevel', 2) !== 0) {
  218. $elemCount = count($arg);
  219. if ($elemCount > 5) {
  220. $arg = array_slice($arg, 0, 5);
  221. $arg[] = 'And ' . ($elemCount - 5) . ' more entries, set log level to debug to see all entries';
  222. }
  223. }
  224. return array_map(function ($e) use ($nestingLevel) {
  225. return $this->encodeArg($e, $nestingLevel - 1);
  226. }, $arg);
  227. }
  228. return $arg;
  229. }
  230. public function serializeException(\Throwable $exception): array {
  231. $data = [
  232. 'Exception' => get_class($exception),
  233. 'Message' => $exception->getMessage(),
  234. 'Code' => $exception->getCode(),
  235. 'Trace' => $this->encodeTrace($exception->getTrace()),
  236. 'File' => $exception->getFile(),
  237. 'Line' => $exception->getLine(),
  238. ];
  239. if ($exception instanceof HintException) {
  240. $data['Hint'] = $exception->getHint();
  241. }
  242. if ($exception->getPrevious()) {
  243. $data['Previous'] = $this->serializeException($exception->getPrevious());
  244. }
  245. return $data;
  246. }
  247. public function enlistSensitiveMethods(string $class, array $methods): void {
  248. if (!isset($this->methodsWithSensitiveParametersByClass[$class])) {
  249. $this->methodsWithSensitiveParametersByClass[$class] = [];
  250. }
  251. $this->methodsWithSensitiveParametersByClass[$class] = array_merge($this->methodsWithSensitiveParametersByClass[$class], $methods);
  252. }
  253. }