SecurityComponent.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  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\Event\Event;
  19. use Cake\Network\Exception\BadRequestException;
  20. use Cake\Network\Request;
  21. use Cake\Utility\Hash;
  22. use Cake\Utility\Security;
  23. /**
  24. * The Security Component creates an easy way to integrate tighter security in
  25. * your application. It provides methods for various tasks like:
  26. *
  27. * - Restricting which HTTP methods your application accepts.
  28. * - Form tampering protection
  29. * - Requiring that SSL be used.
  30. * - Limiting cross controller communication.
  31. *
  32. * @link http://book.cakephp.org/3.0/en/controllers/components/security.html
  33. */
  34. class SecurityComponent extends Component
  35. {
  36. /**
  37. * Default config
  38. *
  39. * - `blackHoleCallback` - The controller method that will be called if this
  40. * request is black-hole'd.
  41. * - `requireSecure` - List of actions that require an SSL-secured connection.
  42. * - `requireAuth` - List of actions that require a valid authentication key.
  43. * - `allowedControllers` - Controllers from which actions of the current
  44. * controller are allowed to receive requests.
  45. * - `allowedActions` - Actions from which actions of the current controller
  46. * are allowed to receive requests.
  47. * - `unlockedFields` - Form fields to exclude from POST validation. Fields can
  48. * be unlocked either in the Component, or with FormHelper::unlockField().
  49. * Fields that have been unlocked are not required to be part of the POST
  50. * and hidden unlocked fields do not have their values checked.
  51. * - `unlockedActions` - Actions to exclude from POST validation checks.
  52. * Other checks like requireAuth(), requireSecure() etc. will still be applied.
  53. * - `validatePost` - Whether to validate POST data. Set to false to disable
  54. * for data coming from 3rd party services, etc.
  55. *
  56. * @var array
  57. */
  58. protected $_defaultConfig = [
  59. 'blackHoleCallback' => null,
  60. 'requireSecure' => [],
  61. 'requireAuth' => [],
  62. 'allowedControllers' => [],
  63. 'allowedActions' => [],
  64. 'unlockedFields' => [],
  65. 'unlockedActions' => [],
  66. 'validatePost' => true
  67. ];
  68. /**
  69. * Holds the current action of the controller
  70. *
  71. * @var string
  72. */
  73. protected $_action = null;
  74. /**
  75. * Request object
  76. *
  77. * @var \Cake\Network\Request
  78. */
  79. public $request;
  80. /**
  81. * The Session object
  82. *
  83. * @var \Cake\Network\Session
  84. */
  85. public $session;
  86. /**
  87. * Component startup. All security checking happens here.
  88. *
  89. * @param Event $event An Event instance
  90. * @return mixed
  91. */
  92. public function startup(Event $event)
  93. {
  94. $controller = $event->subject();
  95. $this->request = $controller->request;
  96. $this->session = $this->request->session();
  97. $this->_action = $this->request->params['action'];
  98. $this->_secureRequired($controller);
  99. $this->_authRequired($controller);
  100. $isPost = $this->request->is(['post', 'put']);
  101. $isNotRequestAction = (
  102. !isset($controller->request->params['requested']) ||
  103. $controller->request->params['requested'] != 1
  104. );
  105. if ($this->_action === $this->_config['blackHoleCallback']) {
  106. return $this->blackHole($controller, 'auth');
  107. }
  108. if (!in_array($this->_action, (array)$this->_config['unlockedActions']) &&
  109. $isPost && $isNotRequestAction
  110. ) {
  111. if ($this->_config['validatePost'] &&
  112. $this->_validatePost($controller) === false
  113. ) {
  114. return $this->blackHole($controller, 'auth');
  115. }
  116. }
  117. $this->generateToken($controller->request);
  118. if ($isPost && is_array($controller->request->data)) {
  119. unset($controller->request->data['_Token']);
  120. }
  121. }
  122. /**
  123. * Events supported by this component.
  124. *
  125. * @return array
  126. */
  127. public function implementedEvents()
  128. {
  129. return [
  130. 'Controller.startup' => 'startup',
  131. ];
  132. }
  133. /**
  134. * Sets the actions that require a request that is SSL-secured, or empty for all actions
  135. *
  136. * @param string|array $actions Actions list
  137. * @return void
  138. */
  139. public function requireSecure($actions = null)
  140. {
  141. $this->_requireMethod('Secure', (array)$actions);
  142. }
  143. /**
  144. * Sets the actions that require whitelisted form submissions.
  145. *
  146. * Adding actions with this method will enforce the restrictions
  147. * set in SecurityComponent::$allowedControllers and
  148. * SecurityComponent::$allowedActions.
  149. *
  150. * @param string|array $actions Actions list
  151. * @return void
  152. */
  153. public function requireAuth($actions)
  154. {
  155. $this->_requireMethod('Auth', (array)$actions);
  156. }
  157. /**
  158. * Black-hole an invalid request with a 400 error or custom callback. If SecurityComponent::$blackHoleCallback
  159. * is specified, it will use this callback by executing the method indicated in $error
  160. *
  161. * @param Controller $controller Instantiating controller
  162. * @param string $error Error method
  163. * @return mixed If specified, controller blackHoleCallback's response, or no return otherwise
  164. * @see SecurityComponent::$blackHoleCallback
  165. * @link http://book.cakephp.org/3.0/en/controllers/components/security.html#handling-blackhole-callbacks
  166. * @throws \Cake\Network\Exception\BadRequestException
  167. */
  168. public function blackHole(Controller $controller, $error = '')
  169. {
  170. if (!$this->_config['blackHoleCallback']) {
  171. throw new BadRequestException('The request has been black-holed');
  172. }
  173. return $this->_callback($controller, $this->_config['blackHoleCallback'], [$error]);
  174. }
  175. /**
  176. * Sets the actions that require a $method HTTP request, or empty for all actions
  177. *
  178. * @param string $method The HTTP method to assign controller actions to
  179. * @param array $actions Controller actions to set the required HTTP method to.
  180. * @return void
  181. */
  182. protected function _requireMethod($method, $actions = [])
  183. {
  184. if (isset($actions[0]) && is_array($actions[0])) {
  185. $actions = $actions[0];
  186. }
  187. $this->config('require' . $method, (empty($actions)) ? ['*'] : $actions);
  188. }
  189. /**
  190. * Check if access requires secure connection
  191. *
  192. * @param Controller $controller Instantiating controller
  193. * @return bool true if secure connection required
  194. */
  195. protected function _secureRequired(Controller $controller)
  196. {
  197. if (is_array($this->_config['requireSecure']) &&
  198. !empty($this->_config['requireSecure'])
  199. ) {
  200. $requireSecure = $this->_config['requireSecure'];
  201. if (in_array($this->_action, $requireSecure) || $requireSecure === ['*']) {
  202. if (!$this->request->is('ssl')) {
  203. if (!$this->blackHole($controller, 'secure')) {
  204. return null;
  205. }
  206. }
  207. }
  208. }
  209. return true;
  210. }
  211. /**
  212. * Check if authentication is required
  213. *
  214. * @param Controller $controller Instantiating controller
  215. * @return bool true if authentication required
  216. */
  217. protected function _authRequired(Controller $controller)
  218. {
  219. if (is_array($this->_config['requireAuth']) &&
  220. !empty($this->_config['requireAuth']) &&
  221. !empty($this->request->data)
  222. ) {
  223. $requireAuth = $this->_config['requireAuth'];
  224. if (in_array($this->request->params['action'], $requireAuth) || $requireAuth == ['*']) {
  225. if (!isset($controller->request->data['_Token'])) {
  226. if (!$this->blackHole($controller, 'auth')) {
  227. return false;
  228. }
  229. }
  230. if ($this->session->check('_Token')) {
  231. $tData = $this->session->read('_Token');
  232. if (!empty($tData['allowedControllers']) &&
  233. !in_array($this->request->params['controller'], $tData['allowedControllers']) ||
  234. !empty($tData['allowedActions']) &&
  235. !in_array($this->request->params['action'], $tData['allowedActions'])
  236. ) {
  237. if (!$this->blackHole($controller, 'auth')) {
  238. return false;
  239. }
  240. }
  241. } else {
  242. if (!$this->blackHole($controller, 'auth')) {
  243. return false;
  244. }
  245. }
  246. }
  247. }
  248. return true;
  249. }
  250. /**
  251. * Validate submitted form
  252. *
  253. * @param Controller $controller Instantiating controller
  254. * @return bool true if submitted form is valid
  255. */
  256. protected function _validatePost(Controller $controller)
  257. {
  258. if (empty($controller->request->data)) {
  259. return true;
  260. }
  261. $check = $controller->request->data;
  262. if (!isset($check['_Token']) ||
  263. !isset($check['_Token']['fields']) ||
  264. !isset($check['_Token']['unlocked'])
  265. ) {
  266. return false;
  267. }
  268. $locked = '';
  269. $token = urldecode($check['_Token']['fields']);
  270. $unlocked = urldecode($check['_Token']['unlocked']);
  271. if (strpos($token, ':')) {
  272. list($token, $locked) = explode(':', $token, 2);
  273. }
  274. unset($check['_Token'], $check['_csrfToken']);
  275. $locked = explode('|', $locked);
  276. $unlocked = explode('|', $unlocked);
  277. $lockedFields = [];
  278. $fields = Hash::flatten($check);
  279. $fieldList = array_keys($fields);
  280. $multi = [];
  281. foreach ($fieldList as $i => $key) {
  282. if (preg_match('/(\.\d){1,10}$/', $key)) {
  283. $multi[$i] = preg_replace('/(\.\d+){1,10}$/', '', $key);
  284. unset($fieldList[$i]);
  285. } else {
  286. $fieldList[$i] = (string)$key;
  287. }
  288. }
  289. if (!empty($multi)) {
  290. $fieldList += array_unique($multi);
  291. }
  292. $unlockedFields = array_unique(
  293. array_merge((array)$this->config('disabledFields'), (array)$this->_config['unlockedFields'], $unlocked)
  294. );
  295. foreach ($fieldList as $i => $key) {
  296. $isLocked = (is_array($locked) && in_array($key, $locked));
  297. if (!empty($unlockedFields)) {
  298. foreach ($unlockedFields as $off) {
  299. $off = explode('.', $off);
  300. $field = array_values(array_intersect(explode('.', $key), $off));
  301. $isUnlocked = ($field === $off);
  302. if ($isUnlocked) {
  303. break;
  304. }
  305. }
  306. }
  307. if ($isUnlocked || $isLocked) {
  308. unset($fieldList[$i]);
  309. if ($isLocked) {
  310. $lockedFields[$key] = $fields[$key];
  311. }
  312. }
  313. }
  314. sort($unlocked, SORT_STRING);
  315. sort($fieldList, SORT_STRING);
  316. ksort($lockedFields, SORT_STRING);
  317. $fieldList += $lockedFields;
  318. $unlocked = implode('|', $unlocked);
  319. $hashParts = [
  320. $controller->request->here(),
  321. serialize($fieldList),
  322. $unlocked,
  323. Security::salt()
  324. ];
  325. $check = Security::hash(implode('', $hashParts), 'sha1');
  326. return ($token === $check);
  327. }
  328. /**
  329. * Manually add form tampering prevention token information into the provided
  330. * request object.
  331. *
  332. * @param \Cake\Network\Request $request The request object to add into.
  333. * @return bool
  334. */
  335. public function generateToken(Request $request)
  336. {
  337. if (isset($request->params['requested']) && $request->params['requested'] === 1) {
  338. if ($this->session->check('_Token')) {
  339. $request->params['_Token'] = $this->session->read('_Token');
  340. }
  341. return false;
  342. }
  343. $token = [
  344. 'allowedControllers' => $this->_config['allowedControllers'],
  345. 'allowedActions' => $this->_config['allowedActions'],
  346. 'unlockedFields' => $this->_config['unlockedFields'],
  347. ];
  348. $this->session->write('_Token', $token);
  349. $request->params['_Token'] = [
  350. 'unlockedFields' => $token['unlockedFields']
  351. ];
  352. return true;
  353. }
  354. /**
  355. * Calls a controller callback method
  356. *
  357. * @param Controller $controller Controller to run callback on
  358. * @param string $method Method to execute
  359. * @param array $params Parameters to send to method
  360. * @return mixed Controller callback method's response
  361. * @throws \Cake\Network\Exception\BadRequestException When a the blackholeCallback is not callable.
  362. */
  363. protected function _callback(Controller $controller, $method, $params = [])
  364. {
  365. if (!is_callable([$controller, $method])) {
  366. throw new BadRequestException('The request has been black-holed');
  367. }
  368. return call_user_func_array([&$controller, $method], empty($params) ? null : $params);
  369. }
  370. }