SecurityComponent.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 0.10.8
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Controller\Component;
  16. use Cake\Controller\Component;
  17. use Cake\Controller\Controller;
  18. use Cake\Controller\Exception\AuthSecurityException;
  19. use Cake\Controller\Exception\SecurityException;
  20. use Cake\Core\Configure;
  21. use Cake\Event\Event;
  22. use Cake\Network\Exception\BadRequestException;
  23. use Cake\Network\Request;
  24. use Cake\Utility\Hash;
  25. use Cake\Utility\Security;
  26. /**
  27. * The Security Component creates an easy way to integrate tighter security in
  28. * your application. It provides methods for various tasks like:
  29. *
  30. * - Restricting which HTTP methods your application accepts.
  31. * - Form tampering protection
  32. * - Requiring that SSL be used.
  33. * - Limiting cross controller communication.
  34. *
  35. * @link http://book.cakephp.org/3.0/en/controllers/components/security.html
  36. */
  37. class SecurityComponent extends Component
  38. {
  39. /**
  40. * Default message used for exceptions thrown
  41. */
  42. const DEFAULT_EXCEPTION_MESSAGE = 'The request has been black-holed';
  43. /**
  44. * Default config
  45. *
  46. * - `blackHoleCallback` - The controller method that will be called if this
  47. * request is black-hole'd.
  48. * - `requireSecure` - List of actions that require an SSL-secured connection.
  49. * - `requireAuth` - List of actions that require a valid authentication key. Deprecated as of 3.2.2
  50. * - `allowedControllers` - Controllers from which actions of the current
  51. * controller are allowed to receive requests.
  52. * - `allowedActions` - Actions from which actions of the current controller
  53. * are allowed to receive requests.
  54. * - `unlockedFields` - Form fields to exclude from POST validation. Fields can
  55. * be unlocked either in the Component, or with FormHelper::unlockField().
  56. * Fields that have been unlocked are not required to be part of the POST
  57. * and hidden unlocked fields do not have their values checked.
  58. * - `unlockedActions` - Actions to exclude from POST validation checks.
  59. * Other checks like requireAuth(), requireSecure() etc. will still be applied.
  60. * - `validatePost` - Whether to validate POST data. Set to false to disable
  61. * for data coming from 3rd party services, etc.
  62. *
  63. * @var array
  64. */
  65. protected $_defaultConfig = [
  66. 'blackHoleCallback' => null,
  67. 'requireSecure' => [],
  68. 'requireAuth' => [],
  69. 'allowedControllers' => [],
  70. 'allowedActions' => [],
  71. 'unlockedFields' => [],
  72. 'unlockedActions' => [],
  73. 'validatePost' => true
  74. ];
  75. /**
  76. * Holds the current action of the controller
  77. *
  78. * @var string
  79. */
  80. protected $_action = null;
  81. /**
  82. * Request object
  83. *
  84. * @var \Cake\Network\Request
  85. */
  86. public $request;
  87. /**
  88. * The Session object
  89. *
  90. * @var \Cake\Network\Session
  91. */
  92. public $session;
  93. /**
  94. * Component startup. All security checking happens here.
  95. *
  96. * @param \Cake\Event\Event $event An Event instance
  97. * @return mixed
  98. */
  99. public function startup(Event $event)
  100. {
  101. $controller = $event->subject();
  102. $this->session = $this->request->session();
  103. $this->_action = $this->request->params['action'];
  104. $hasData = !empty($this->request->data);
  105. try {
  106. $this->_secureRequired($controller);
  107. $this->_authRequired($controller);
  108. $isNotRequestAction = (
  109. !isset($controller->request->params['requested']) ||
  110. $controller->request->params['requested'] != 1
  111. );
  112. if ($this->_action === $this->_config['blackHoleCallback']) {
  113. throw new AuthSecurityException(sprintf('Action %s is defined as the blackhole callback.', $this->_action));
  114. }
  115. if (!in_array($this->_action, (array)$this->_config['unlockedActions']) &&
  116. $hasData &&
  117. $isNotRequestAction &&
  118. $this->_config['validatePost']) {
  119. $this->_validatePost($controller);
  120. }
  121. } catch (SecurityException $se) {
  122. $this->blackHole($controller, $se->getType(), $se);
  123. }
  124. $this->generateToken($controller->request);
  125. if ($hasData && is_array($controller->request->data)) {
  126. unset($controller->request->data['_Token']);
  127. }
  128. }
  129. /**
  130. * Events supported by this component.
  131. *
  132. * @return array
  133. */
  134. public function implementedEvents()
  135. {
  136. return [
  137. 'Controller.startup' => 'startup',
  138. ];
  139. }
  140. /**
  141. * Sets the actions that require a request that is SSL-secured, or empty for all actions
  142. *
  143. * @param string|array|null $actions Actions list
  144. * @return void
  145. */
  146. public function requireSecure($actions = null)
  147. {
  148. $this->_requireMethod('Secure', (array)$actions);
  149. }
  150. /**
  151. * Sets the actions that require whitelisted form submissions.
  152. *
  153. * Adding actions with this method will enforce the restrictions
  154. * set in SecurityComponent::$allowedControllers and
  155. * SecurityComponent::$allowedActions.
  156. *
  157. * @param string|array $actions Actions list
  158. * @return void
  159. * @deprecated 3.2.2 This feature is confusing and not useful.
  160. */
  161. public function requireAuth($actions)
  162. {
  163. $this->_requireMethod('Auth', (array)$actions);
  164. }
  165. /**
  166. * Black-hole an invalid request with a 400 error or custom callback. If SecurityComponent::$blackHoleCallback
  167. * is specified, it will use this callback by executing the method indicated in $error
  168. *
  169. * @param \Cake\Controller\Controller $controller Instantiating controller
  170. * @param string $error Error method
  171. * @param \Cake\Controller\Exception\SecurityException $exception Additional debug info describing the cause
  172. * @return mixed If specified, controller blackHoleCallback's response, or no return otherwise
  173. * @see \Cake\Controller\Component\SecurityComponent::$blackHoleCallback
  174. * @link http://book.cakephp.org/3.0/en/controllers/components/security.html#handling-blackhole-callbacks
  175. * @throws \Cake\Network\Exception\BadRequestException
  176. */
  177. public function blackHole(Controller $controller, $error = '', SecurityException $exception = null)
  178. {
  179. if (!$this->_config['blackHoleCallback']) {
  180. $this->_throwException($exception);
  181. }
  182. return $this->_callback($controller, $this->_config['blackHoleCallback'], [$error, $exception]);
  183. }
  184. /**
  185. * Check debug status and throw an Exception based on the existing one
  186. *
  187. * @param \Cake\Controller\Exception\SecurityException $exception Additional debug info describing the cause
  188. * @throws \Cake\Network\Exception\BadRequestException
  189. * @return void
  190. */
  191. protected function _throwException($exception = null)
  192. {
  193. if ($exception !== null) {
  194. if (!Configure::read('debug') && $exception instanceof SecurityException) {
  195. $exception->setReason($exception->getMessage());
  196. $exception->setMessage(self::DEFAULT_EXCEPTION_MESSAGE);
  197. }
  198. throw $exception;
  199. }
  200. throw new BadRequestException(self::DEFAULT_EXCEPTION_MESSAGE);
  201. }
  202. /**
  203. * Sets the actions that require a $method HTTP request, or empty for all actions
  204. *
  205. * @param string $method The HTTP method to assign controller actions to
  206. * @param array $actions Controller actions to set the required HTTP method to.
  207. * @return void
  208. */
  209. protected function _requireMethod($method, $actions = [])
  210. {
  211. if (isset($actions[0]) && is_array($actions[0])) {
  212. $actions = $actions[0];
  213. }
  214. $this->config('require' . $method, (empty($actions)) ? ['*'] : $actions);
  215. }
  216. /**
  217. * Check if access requires secure connection
  218. *
  219. * @param \Cake\Controller\Controller $controller Instantiating controller
  220. * @return bool true if secure connection required
  221. */
  222. protected function _secureRequired(Controller $controller)
  223. {
  224. if (is_array($this->_config['requireSecure']) &&
  225. !empty($this->_config['requireSecure'])
  226. ) {
  227. $requireSecure = $this->_config['requireSecure'];
  228. if (in_array($this->_action, $requireSecure) || $requireSecure === ['*']) {
  229. if (!$this->request->is('ssl')) {
  230. throw new SecurityException(
  231. 'Request is not SSL and the action is required to be secure'
  232. );
  233. }
  234. }
  235. }
  236. return true;
  237. }
  238. /**
  239. * Check if authentication is required
  240. *
  241. * @param \Cake\Controller\Controller $controller Instantiating controller
  242. * @return bool true if authentication required
  243. * @deprecated 3.2.2 This feature is confusing and not useful.
  244. */
  245. protected function _authRequired(Controller $controller)
  246. {
  247. if (is_array($this->_config['requireAuth']) &&
  248. !empty($this->_config['requireAuth']) &&
  249. !empty($this->request->data)
  250. ) {
  251. $requireAuth = $this->_config['requireAuth'];
  252. if (in_array($this->request->params['action'], $requireAuth) || $requireAuth == ['*']) {
  253. if (!isset($controller->request->data['_Token'])) {
  254. throw new AuthSecurityException('\'_Token\' was not found in request data.');
  255. }
  256. if ($this->session->check('_Token')) {
  257. $tData = $this->session->read('_Token');
  258. if (!empty($tData['allowedControllers']) &&
  259. !in_array($this->request->params['controller'], $tData['allowedControllers'])) {
  260. throw new AuthSecurityException(
  261. sprintf(
  262. 'Controller \'%s\' was not found in allowed controllers: \'%s\'.',
  263. $this->request->params['controller'],
  264. implode(', ', (array)$tData['allowedControllers'])
  265. )
  266. );
  267. }
  268. if (!empty($tData['allowedActions']) &&
  269. !in_array($this->request->params['action'], $tData['allowedActions'])
  270. ) {
  271. throw new AuthSecurityException(
  272. sprintf(
  273. 'Action \'%s::%s\' was not found in allowed actions: \'%s\'.',
  274. $this->request->params['controller'],
  275. $this->request->params['action'],
  276. implode(', ', (array)$tData['allowedActions'])
  277. )
  278. );
  279. }
  280. } else {
  281. throw new AuthSecurityException('\'_Token\' was not found in session.');
  282. }
  283. }
  284. }
  285. return true;
  286. }
  287. /**
  288. * Validate submitted form
  289. *
  290. * @param \Cake\Controller\Controller $controller Instantiating controller
  291. * @throws \Cake\Controller\Exception\AuthSecurityException
  292. * @return bool true if submitted form is valid
  293. */
  294. protected function _validatePost(Controller $controller)
  295. {
  296. if (empty($controller->request->data)) {
  297. return true;
  298. }
  299. $token = $this->_validToken($controller);
  300. $hashParts = $this->_hashParts($controller);
  301. $check = Security::hash(implode('', $hashParts), 'sha1');
  302. if ($token === $check) {
  303. return true;
  304. }
  305. $msg = self::DEFAULT_EXCEPTION_MESSAGE;
  306. if (Configure::read('debug')) {
  307. $msg = $this->_debugPostTokenNotMatching($controller, $hashParts);
  308. }
  309. throw new AuthSecurityException($msg);
  310. }
  311. /**
  312. * Check if token is valid
  313. *
  314. * @param \Cake\Controller\Controller $controller Instantiating controller
  315. * @throws \Cake\Controller\Exception\SecurityException
  316. * @return string fields token
  317. */
  318. protected function _validToken(Controller $controller)
  319. {
  320. $check = $controller->request->data;
  321. $message = '\'%s\' was not found in request data.';
  322. if (!isset($check['_Token'])) {
  323. throw new AuthSecurityException(sprintf($message, '_Token'));
  324. }
  325. if (!isset($check['_Token']['fields'])) {
  326. throw new AuthSecurityException(sprintf($message, '_Token.fields'));
  327. }
  328. if (!isset($check['_Token']['unlocked'])) {
  329. throw new AuthSecurityException(sprintf($message, '_Token.unlocked'));
  330. }
  331. if (Configure::read('debug') && !isset($check['_Token']['debug'])) {
  332. throw new SecurityException(sprintf($message, '_Token.debug'));
  333. }
  334. if (!Configure::read('debug') && isset($check['_Token']['debug'])) {
  335. throw new SecurityException('Unexpected \'_Token.debug\' found in request data');
  336. }
  337. $token = urldecode($check['_Token']['fields']);
  338. if (strpos($token, ':')) {
  339. list($token, ) = explode(':', $token, 2);
  340. }
  341. return $token;
  342. }
  343. /**
  344. * Return hash parts for the Token generation
  345. *
  346. * @param \Cake\Controller\Controller $controller Instantiating controller
  347. * @return array
  348. */
  349. protected function _hashParts(Controller $controller)
  350. {
  351. $fieldList = $this->_fieldsList($controller->request->data);
  352. $unlocked = $this->_sortedUnlocked($controller->request->data);
  353. return [
  354. $controller->request->here(),
  355. serialize($fieldList),
  356. $unlocked,
  357. Security::salt()
  358. ];
  359. }
  360. /**
  361. * Return the fields list for the hash calculation
  362. *
  363. * @param array $check Data array
  364. * @return array
  365. */
  366. protected function _fieldsList(array $check)
  367. {
  368. $locked = '';
  369. $token = urldecode($check['_Token']['fields']);
  370. $unlocked = $this->_unlocked($check);
  371. if (strpos($token, ':')) {
  372. list($token, $locked) = explode(':', $token, 2);
  373. }
  374. unset($check['_Token'], $check['_csrfToken']);
  375. $locked = explode('|', $locked);
  376. $unlocked = explode('|', $unlocked);
  377. $fields = Hash::flatten($check);
  378. $fieldList = array_keys($fields);
  379. $multi = $lockedFields = [];
  380. $isUnlocked = false;
  381. foreach ($fieldList as $i => $key) {
  382. if (preg_match('/(\.\d+){1,10}$/', $key)) {
  383. $multi[$i] = preg_replace('/(\.\d+){1,10}$/', '', $key);
  384. unset($fieldList[$i]);
  385. } else {
  386. $fieldList[$i] = (string)$key;
  387. }
  388. }
  389. if (!empty($multi)) {
  390. $fieldList += array_unique($multi);
  391. }
  392. $unlockedFields = array_unique(
  393. array_merge((array)$this->config('disabledFields'), (array)$this->_config['unlockedFields'], $unlocked)
  394. );
  395. foreach ($fieldList as $i => $key) {
  396. $isLocked = (is_array($locked) && in_array($key, $locked));
  397. if (!empty($unlockedFields)) {
  398. foreach ($unlockedFields as $off) {
  399. $off = explode('.', $off);
  400. $field = array_values(array_intersect(explode('.', $key), $off));
  401. $isUnlocked = ($field === $off);
  402. if ($isUnlocked) {
  403. break;
  404. }
  405. }
  406. }
  407. if ($isUnlocked || $isLocked) {
  408. unset($fieldList[$i]);
  409. if ($isLocked) {
  410. $lockedFields[$key] = $fields[$key];
  411. }
  412. }
  413. }
  414. sort($fieldList, SORT_STRING);
  415. ksort($lockedFields, SORT_STRING);
  416. $fieldList += $lockedFields;
  417. return $fieldList;
  418. }
  419. /**
  420. * Get the unlocked string
  421. *
  422. * @param array $data Data array
  423. * @return string
  424. */
  425. protected function _unlocked(array $data)
  426. {
  427. return urldecode($data['_Token']['unlocked']);
  428. }
  429. /**
  430. * Get the sorted unlocked string
  431. *
  432. * @param array $data Data array
  433. * @return string
  434. */
  435. protected function _sortedUnlocked($data)
  436. {
  437. $unlocked = $this->_unlocked($data);
  438. $unlocked = explode('|', $unlocked);
  439. sort($unlocked, SORT_STRING);
  440. return implode('|', $unlocked);
  441. }
  442. /**
  443. * Create a message for humans to understand why Security token is not matching
  444. *
  445. * @param \Cake\Controller\Controller $controller Instantiating controller
  446. * @param array $hashParts Elements used to generate the Token hash
  447. * @return string Message explaining why the tokens are not matching
  448. */
  449. protected function _debugPostTokenNotMatching(Controller $controller, $hashParts)
  450. {
  451. $messages = [];
  452. $expectedParts = json_decode(urldecode($controller->request->data['_Token']['debug']), true);
  453. if (!is_array($expectedParts) || count($expectedParts) !== 3) {
  454. return 'Invalid security debug token.';
  455. }
  456. $expectedUrl = Hash::get($expectedParts, 0);
  457. $url = Hash::get($hashParts, 0);
  458. if ($expectedUrl !== $url) {
  459. $messages[] = sprintf('URL mismatch in POST data (expected \'%s\' but found \'%s\')', $expectedUrl, $url);
  460. }
  461. $expectedFields = Hash::get($expectedParts, 1);
  462. $dataFields = Hash::get($hashParts, 1);
  463. if ($dataFields) {
  464. $dataFields = unserialize($dataFields);
  465. }
  466. $fieldsMessages = $this->_debugCheckFields(
  467. $dataFields,
  468. $expectedFields,
  469. 'Unexpected field \'%s\' in POST data',
  470. 'Tampered field \'%s\' in POST data (expected value \'%s\' but found \'%s\')',
  471. 'Missing field \'%s\' in POST data'
  472. );
  473. $expectedUnlockedFields = Hash::get($expectedParts, 2);
  474. $dataUnlockedFields = Hash::get($hashParts, 2) ?: [];
  475. if ($dataUnlockedFields) {
  476. $dataUnlockedFields = explode('|', $dataUnlockedFields);
  477. }
  478. $unlockFieldsMessages = $this->_debugCheckFields(
  479. $dataUnlockedFields,
  480. $expectedUnlockedFields,
  481. 'Unexpected unlocked field \'%s\' in POST data',
  482. null,
  483. 'Missing unlocked field: \'%s\''
  484. );
  485. $messages = array_merge($messages, $fieldsMessages, $unlockFieldsMessages);
  486. return implode(', ', $messages);
  487. }
  488. /**
  489. * Iterates data array to check against expected
  490. *
  491. * @param array $dataFields Fields array, containing the POST data fields
  492. * @param array $expectedFields Fields array, containing the expected fields we should have in POST
  493. * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected)
  494. * @param string $stringKeyMessage Message string if tampered found in data fields indexed by string (protected)
  495. * @param string $missingMessage Message string if missing field
  496. * @return array Messages
  497. */
  498. protected function _debugCheckFields($dataFields, $expectedFields = [], $intKeyMessage = '', $stringKeyMessage = '', $missingMessage = '')
  499. {
  500. $messages = $this->_matchExistingFields($dataFields, $expectedFields, $intKeyMessage, $stringKeyMessage);
  501. $expectedFieldsMessage = $this->_debugExpectedFields($expectedFields, $missingMessage);
  502. if ($expectedFieldsMessage !== null) {
  503. $messages[] = $expectedFieldsMessage;
  504. }
  505. return $messages;
  506. }
  507. /**
  508. * Manually add form tampering prevention token information into the provided
  509. * request object.
  510. *
  511. * @param \Cake\Network\Request $request The request object to add into.
  512. * @return bool
  513. */
  514. public function generateToken(Request $request)
  515. {
  516. if (isset($request->params['requested']) && $request->params['requested'] === 1) {
  517. if ($this->session->check('_Token')) {
  518. $request->params['_Token'] = $this->session->read('_Token');
  519. }
  520. return false;
  521. }
  522. $token = [
  523. 'allowedControllers' => $this->_config['allowedControllers'],
  524. 'allowedActions' => $this->_config['allowedActions'],
  525. 'unlockedFields' => $this->_config['unlockedFields'],
  526. ];
  527. $this->session->write('_Token', $token);
  528. $request->params['_Token'] = [
  529. 'unlockedFields' => $token['unlockedFields']
  530. ];
  531. return true;
  532. }
  533. /**
  534. * Calls a controller callback method
  535. *
  536. * @param \Cake\Controller\Controller $controller Instantiating controller
  537. * @param string $method Method to execute
  538. * @param array $params Parameters to send to method
  539. * @return mixed Controller callback method's response
  540. * @throws \Cake\Network\Exception\BadRequestException When a the blackholeCallback is not callable.
  541. */
  542. protected function _callback(Controller $controller, $method, $params = [])
  543. {
  544. if (!is_callable([$controller, $method])) {
  545. throw new BadRequestException('The request has been black-holed');
  546. }
  547. return call_user_func_array([&$controller, $method], empty($params) ? null : $params);
  548. }
  549. /**
  550. * Generate array of messages for the existing fields in POST data, matching dataFields in $expectedFields
  551. * will be unset
  552. *
  553. * @param array $dataFields Fields array, containing the POST data fields
  554. * @param array $expectedFields Fields array, containing the expected fields we should have in POST
  555. * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected)
  556. * @param string $stringKeyMessage Message string if tampered found in data fields indexed by string (protected)
  557. * @return array Error messages
  558. */
  559. protected function _matchExistingFields($dataFields, &$expectedFields, $intKeyMessage, $stringKeyMessage)
  560. {
  561. $messages = [];
  562. foreach ((array)$dataFields as $key => $value) {
  563. if (is_int($key)) {
  564. $foundKey = array_search($value, (array)$expectedFields);
  565. if ($foundKey === false) {
  566. $messages[] = sprintf($intKeyMessage, $value);
  567. } else {
  568. unset($expectedFields[$foundKey]);
  569. }
  570. } elseif (is_string($key)) {
  571. if (isset($expectedFields[$key]) && $value !== $expectedFields[$key]) {
  572. $messages[] = sprintf($stringKeyMessage, $key, $expectedFields[$key], $value);
  573. }
  574. unset($expectedFields[$key]);
  575. }
  576. }
  577. return $messages;
  578. }
  579. /**
  580. * Generate debug message for the expected fields
  581. *
  582. * @param array $expectedFields Expected fields
  583. * @param string $missingMessage Message template
  584. * @return string Error message about expected fields
  585. */
  586. protected function _debugExpectedFields($expectedFields = [], $missingMessage = '')
  587. {
  588. if (count($expectedFields) === 0) {
  589. return null;
  590. }
  591. $expectedFieldNames = [];
  592. foreach ((array)$expectedFields as $key => $expectedField) {
  593. if (is_int($key)) {
  594. $expectedFieldNames[] = $expectedField;
  595. } else {
  596. $expectedFieldNames[] = $key;
  597. }
  598. }
  599. return sprintf($missingMessage, implode(', ', $expectedFieldNames));
  600. }
  601. }