ExceptionSerializer.php 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
  4. *
  5. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  6. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  7. * @author Joas Schilling <coding@schilljs.com>
  8. * @author Morris Jobke <hey@morrisjobke.de>
  9. * @author Robin Appelman <robin@icewind.nl>
  10. * @author Roeland Jago Douma <roeland@famdouma.nl>
  11. * @author Vincent Petry <vincent@nextcloud.com>
  12. *
  13. * @license GNU AGPL version 3 or any later version
  14. *
  15. * This program is free software: you can redistribute it and/or modify
  16. * it under the terms of the GNU Affero General Public License as
  17. * published by the Free Software Foundation, either version 3 of the
  18. * License, or (at your option) any later version.
  19. *
  20. * This program is distributed in the hope that it will be useful,
  21. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  22. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  23. * GNU Affero General Public License for more details.
  24. *
  25. * You should have received a copy of the GNU Affero General Public License
  26. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  27. *
  28. */
  29. namespace OC\Log;
  30. use OC\Core\Controller\SetupController;
  31. use OC\Http\Client\Client;
  32. use OC\Security\IdentityProof\Key;
  33. use OC\Setup;
  34. use OC\SystemConfig;
  35. use OCA\Encryption\Controller\RecoveryController;
  36. use OCA\Encryption\Controller\SettingsController;
  37. use OCA\Encryption\Crypto\Crypt;
  38. use OCA\Encryption\Crypto\Encryption;
  39. use OCA\Encryption\Hooks\UserHooks;
  40. use OCA\Encryption\KeyManager;
  41. use OCA\Encryption\Session;
  42. use OCP\HintException;
  43. class ExceptionSerializer {
  44. public const SENSITIVE_VALUE_PLACEHOLDER = '*** sensitive parameters replaced ***';
  45. public const methodsWithSensitiveParameters = [
  46. // Session/User
  47. 'completeLogin',
  48. 'login',
  49. 'checkPassword',
  50. 'checkPasswordNoLogging',
  51. 'loginWithPassword',
  52. 'updatePrivateKeyPassword',
  53. 'validateUserPass',
  54. 'loginWithToken',
  55. '{closure}',
  56. 'createSessionToken',
  57. // Provisioning
  58. 'addUser',
  59. // TokenProvider
  60. 'getToken',
  61. 'isTokenPassword',
  62. 'getPassword',
  63. 'decryptPassword',
  64. 'logClientIn',
  65. 'generateToken',
  66. 'validateToken',
  67. // TwoFactorAuth
  68. 'solveChallenge',
  69. 'verifyChallenge',
  70. // ICrypto
  71. 'calculateHMAC',
  72. 'encrypt',
  73. 'decrypt',
  74. // LoginController
  75. 'tryLogin',
  76. 'confirmPassword',
  77. // LDAP
  78. 'bind',
  79. 'areCredentialsValid',
  80. 'invokeLDAPMethod',
  81. // Encryption
  82. 'storeKeyPair',
  83. 'setupUser',
  84. 'checkSignature',
  85. // files_external: OCA\Files_External\MountConfig
  86. 'getBackendStatus',
  87. // files_external: UserStoragesController
  88. 'update',
  89. // Preview providers, don't log big data strings
  90. 'imagecreatefromstring',
  91. // text: PublicSessionController, SessionController and ApiService
  92. 'create',
  93. 'close',
  94. 'push',
  95. 'sync',
  96. 'updateSession',
  97. 'mention',
  98. 'loginSessionUser',
  99. ];
  100. public function __construct(
  101. private SystemConfig $systemConfig,
  102. ) {
  103. }
  104. protected array $methodsWithSensitiveParametersByClass = [
  105. SetupController::class => [
  106. 'run',
  107. 'display',
  108. 'loadAutoConfig',
  109. ],
  110. Setup::class => [
  111. 'install'
  112. ],
  113. Key::class => [
  114. '__construct'
  115. ],
  116. Client::class => [
  117. 'request',
  118. 'delete',
  119. 'deleteAsync',
  120. 'get',
  121. 'getAsync',
  122. 'head',
  123. 'headAsync',
  124. 'options',
  125. 'optionsAsync',
  126. 'patch',
  127. 'post',
  128. 'postAsync',
  129. 'put',
  130. 'putAsync',
  131. ],
  132. \Redis::class => [
  133. 'auth'
  134. ],
  135. \RedisCluster::class => [
  136. '__construct'
  137. ],
  138. Crypt::class => [
  139. 'symmetricEncryptFileContent',
  140. 'encrypt',
  141. 'generatePasswordHash',
  142. 'encryptPrivateKey',
  143. 'decryptPrivateKey',
  144. 'isValidPrivateKey',
  145. 'symmetricDecryptFileContent',
  146. 'checkSignature',
  147. 'createSignature',
  148. 'decrypt',
  149. 'multiKeyDecrypt',
  150. 'multiKeyEncrypt',
  151. ],
  152. RecoveryController::class => [
  153. 'adminRecovery',
  154. 'changeRecoveryPassword'
  155. ],
  156. SettingsController::class => [
  157. 'updatePrivateKeyPassword',
  158. ],
  159. Encryption::class => [
  160. 'encrypt',
  161. 'decrypt',
  162. ],
  163. KeyManager::class => [
  164. 'checkRecoveryPassword',
  165. 'storeKeyPair',
  166. 'setRecoveryKey',
  167. 'setPrivateKey',
  168. 'setFileKey',
  169. 'setAllFileKeys',
  170. ],
  171. Session::class => [
  172. 'setPrivateKey',
  173. 'prepareDecryptAll',
  174. ],
  175. \OCA\Encryption\Users\Setup::class => [
  176. 'setupUser',
  177. ],
  178. UserHooks::class => [
  179. 'login',
  180. 'postCreateUser',
  181. 'postDeleteUser',
  182. 'prePasswordReset',
  183. 'postPasswordReset',
  184. 'preSetPassphrase',
  185. 'setPassphrase',
  186. ],
  187. ];
  188. private function editTrace(array &$sensitiveValues, array $traceLine): array {
  189. if (isset($traceLine['args'])) {
  190. $sensitiveValues = array_merge($sensitiveValues, $traceLine['args']);
  191. }
  192. $traceLine['args'] = [self::SENSITIVE_VALUE_PLACEHOLDER];
  193. return $traceLine;
  194. }
  195. private function filterTrace(array $trace) {
  196. $sensitiveValues = [];
  197. $trace = array_map(function (array $traceLine) use (&$sensitiveValues) {
  198. $className = $traceLine['class'] ?? '';
  199. if ($className && isset($this->methodsWithSensitiveParametersByClass[$className])
  200. && in_array($traceLine['function'], $this->methodsWithSensitiveParametersByClass[$className], true)) {
  201. return $this->editTrace($sensitiveValues, $traceLine);
  202. }
  203. foreach (self::methodsWithSensitiveParameters as $sensitiveMethod) {
  204. if (str_contains($traceLine['function'], $sensitiveMethod)) {
  205. return $this->editTrace($sensitiveValues, $traceLine);
  206. }
  207. }
  208. return $traceLine;
  209. }, $trace);
  210. return array_map(function (array $traceLine) use ($sensitiveValues) {
  211. if (isset($traceLine['args'])) {
  212. $traceLine['args'] = $this->removeValuesFromArgs($traceLine['args'], $sensitiveValues);
  213. }
  214. return $traceLine;
  215. }, $trace);
  216. }
  217. private function removeValuesFromArgs($args, $values): array {
  218. $workArgs = [];
  219. foreach ($args as $key => $arg) {
  220. if (in_array($arg, $values, true)) {
  221. $arg = self::SENSITIVE_VALUE_PLACEHOLDER;
  222. } elseif (is_array($arg)) {
  223. $arg = $this->removeValuesFromArgs($arg, $values);
  224. }
  225. $workArgs[$key] = $arg;
  226. }
  227. return $workArgs;
  228. }
  229. private function encodeTrace($trace) {
  230. $trace = array_map(function (array $line) {
  231. if (isset($line['args'])) {
  232. $line['args'] = array_map([$this, 'encodeArg'], $line['args']);
  233. }
  234. return $line;
  235. }, $trace);
  236. return $this->filterTrace($trace);
  237. }
  238. private function encodeArg($arg, $nestingLevel = 5) {
  239. if (is_object($arg)) {
  240. if ($nestingLevel === 0) {
  241. return [
  242. '__class__' => get_class($arg),
  243. '__properties__' => 'Encoding skipped as the maximum nesting level was reached',
  244. ];
  245. }
  246. $objectInfo = [ '__class__' => get_class($arg) ];
  247. $objectVars = get_object_vars($arg);
  248. return array_map(function ($arg) use ($nestingLevel) {
  249. return $this->encodeArg($arg, $nestingLevel - 1);
  250. }, array_merge($objectInfo, $objectVars));
  251. }
  252. if (is_array($arg)) {
  253. if ($nestingLevel === 0) {
  254. return ['Encoding skipped as the maximum nesting level was reached'];
  255. }
  256. // Only log the first 5 elements of an array unless we are on debug
  257. if ((int)$this->systemConfig->getValue('loglevel', 2) !== 0) {
  258. $elemCount = count($arg);
  259. if ($elemCount > 5) {
  260. $arg = array_slice($arg, 0, 5);
  261. $arg[] = 'And ' . ($elemCount - 5) . ' more entries, set log level to debug to see all entries';
  262. }
  263. }
  264. return array_map(function ($e) use ($nestingLevel) {
  265. return $this->encodeArg($e, $nestingLevel - 1);
  266. }, $arg);
  267. }
  268. return $arg;
  269. }
  270. public function serializeException(\Throwable $exception): array {
  271. $data = [
  272. 'Exception' => get_class($exception),
  273. 'Message' => $exception->getMessage(),
  274. 'Code' => $exception->getCode(),
  275. 'Trace' => $this->encodeTrace($exception->getTrace()),
  276. 'File' => $exception->getFile(),
  277. 'Line' => $exception->getLine(),
  278. ];
  279. if ($exception instanceof HintException) {
  280. $data['Hint'] = $exception->getHint();
  281. }
  282. if ($exception->getPrevious()) {
  283. $data['Previous'] = $this->serializeException($exception->getPrevious());
  284. }
  285. return $data;
  286. }
  287. public function enlistSensitiveMethods(string $class, array $methods): void {
  288. if (!isset($this->methodsWithSensitiveParametersByClass[$class])) {
  289. $this->methodsWithSensitiveParametersByClass[$class] = [];
  290. }
  291. $this->methodsWithSensitiveParametersByClass[$class] = array_merge($this->methodsWithSensitiveParametersByClass[$class], $methods);
  292. }
  293. }