SecurityComponent.php 12 KB

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