PageRenderTime 54ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/cake/libs/controller/components/security.php

https://github.com/GuillaumeLarroque/festimpro
PHP | 739 lines | 369 code | 73 blank | 297 comment | 106 complexity | e751a8c4e1962cda6ce2d641d5b964f5 MD5 | raw file
  1. <?php
  2. /**
  3. * Short description for file.
  4. *
  5. * PHP versions 4 and 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright 2005-2009, Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * Redistributions of files must retain the above copyright notice.
  12. *
  13. * @copyright Copyright 2005-2009, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14. * @link http://cakephp.org CakePHP(tm) Project
  15. * @package cake
  16. * @subpackage cake.cake.libs.controller.components
  17. * @since CakePHP(tm) v 0.10.8.2156
  18. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  19. */
  20. /**
  21. * Short description for file.
  22. *
  23. * @package cake
  24. * @subpackage cake.cake.libs.controller.components
  25. */
  26. class SecurityComponent extends Object {
  27. /**
  28. * The controller method that will be called if this request is black-hole'd
  29. *
  30. * @var string
  31. * @access public
  32. */
  33. var $blackHoleCallback = null;
  34. /**
  35. * List of controller actions for which a POST request is required
  36. *
  37. * @var array
  38. * @access public
  39. * @see SecurityComponent::requirePost()
  40. */
  41. var $requirePost = array();
  42. /**
  43. * List of controller actions for which a GET request is required
  44. *
  45. * @var array
  46. * @access public
  47. * @see SecurityComponent::requireGet()
  48. */
  49. var $requireGet = array();
  50. /**
  51. * List of controller actions for which a PUT request is required
  52. *
  53. * @var array
  54. * @access public
  55. * @see SecurityComponent::requirePut()
  56. */
  57. var $requirePut = array();
  58. /**
  59. * List of controller actions for which a DELETE request is required
  60. *
  61. * @var array
  62. * @access public
  63. * @see SecurityComponent::requireDelete()
  64. */
  65. var $requireDelete = array();
  66. /**
  67. * List of actions that require an SSL-secured connection
  68. *
  69. * @var array
  70. * @access public
  71. * @see SecurityComponent::requireSecure()
  72. */
  73. var $requireSecure = array();
  74. /**
  75. * List of actions that require a valid authentication key
  76. *
  77. * @var array
  78. * @access public
  79. * @see SecurityComponent::requireAuth()
  80. */
  81. var $requireAuth = array();
  82. /**
  83. * List of actions that require an HTTP-authenticated login (basic or digest)
  84. *
  85. * @var array
  86. * @access public
  87. * @see SecurityComponent::requireLogin()
  88. */
  89. var $requireLogin = array();
  90. /**
  91. * Login options for SecurityComponent::requireLogin()
  92. *
  93. * @var array
  94. * @access public
  95. * @see SecurityComponent::requireLogin()
  96. */
  97. var $loginOptions = array('type' => '', 'prompt' => null);
  98. /**
  99. * An associative array of usernames/passwords used for HTTP-authenticated logins.
  100. * If using digest authentication, passwords should be MD5-hashed.
  101. *
  102. * @var array
  103. * @access public
  104. * @see SecurityComponent::requireLogin()
  105. */
  106. var $loginUsers = array();
  107. /**
  108. * Controllers from which actions of the current controller are allowed to receive
  109. * requests.
  110. *
  111. * @var array
  112. * @access public
  113. * @see SecurityComponent::requireAuth()
  114. */
  115. var $allowedControllers = array();
  116. /**
  117. * Actions from which actions of the current controller are allowed to receive
  118. * requests.
  119. *
  120. * @var array
  121. * @access public
  122. * @see SecurityComponent::requireAuth()
  123. */
  124. var $allowedActions = array();
  125. /**
  126. * Form fields to disable
  127. *
  128. * @var array
  129. * @access public
  130. */
  131. var $disabledFields = array();
  132. /**
  133. * Whether to validate POST data. Set to false to disable for data coming from 3rd party
  134. * services, etc.
  135. *
  136. * @var boolean
  137. * @access public
  138. */
  139. var $validatePost = true;
  140. /**
  141. * Other components used by the Security component
  142. *
  143. * @var array
  144. * @access public
  145. */
  146. var $components = array('RequestHandler', 'Session');
  147. /**
  148. * Holds the current action of the controller
  149. *
  150. * @var string
  151. */
  152. var $_action = null;
  153. /**
  154. * Initialize the SecurityComponent
  155. *
  156. * @param object $controller Controller instance for the request
  157. * @param array $settings Settings to set to the component
  158. * @return void
  159. * @access public
  160. */
  161. function initialize(&$controller, $settings = array()) {
  162. $this->_set($settings);
  163. }
  164. /**
  165. * Component startup. All security checking happens here.
  166. *
  167. * @param object $controller Instantiating controller
  168. * @return void
  169. * @access public
  170. */
  171. function startup(&$controller) {
  172. $this->_action = strtolower($controller->action);
  173. $this->_methodsRequired($controller);
  174. $this->_secureRequired($controller);
  175. $this->_authRequired($controller);
  176. $this->_loginRequired($controller);
  177. $isPost = ($this->RequestHandler->isPost() || $this->RequestHandler->isPut());
  178. $isRequestAction = (
  179. !isset($controller->params['requested']) ||
  180. $controller->params['requested'] != 1
  181. );
  182. if ($isPost && $isRequestAction && $this->validatePost) {
  183. if ($this->_validatePost($controller) === false) {
  184. if (!$this->blackHole($controller, 'auth')) {
  185. return null;
  186. }
  187. }
  188. }
  189. $this->_generateToken($controller);
  190. }
  191. /**
  192. * Sets the actions that require a POST request, or empty for all actions
  193. *
  194. * @return void
  195. * @access public
  196. */
  197. function requirePost() {
  198. $args = func_get_args();
  199. $this->_requireMethod('Post', $args);
  200. }
  201. /**
  202. * Sets the actions that require a GET request, or empty for all actions
  203. *
  204. * @return void
  205. * @access public
  206. */
  207. function requireGet() {
  208. $args = func_get_args();
  209. $this->_requireMethod('Get', $args);
  210. }
  211. /**
  212. * Sets the actions that require a PUT request, or empty for all actions
  213. *
  214. * @return void
  215. * @access public
  216. */
  217. function requirePut() {
  218. $args = func_get_args();
  219. $this->_requireMethod('Put', $args);
  220. }
  221. /**
  222. * Sets the actions that require a DELETE request, or empty for all actions
  223. *
  224. * @return void
  225. * @access public
  226. */
  227. function requireDelete() {
  228. $args = func_get_args();
  229. $this->_requireMethod('Delete', $args);
  230. }
  231. /**
  232. * Sets the actions that require a request that is SSL-secured, or empty for all actions
  233. *
  234. * @return void
  235. * @access public
  236. */
  237. function requireSecure() {
  238. $args = func_get_args();
  239. $this->_requireMethod('Secure', $args);
  240. }
  241. /**
  242. * Sets the actions that require an authenticated request, or empty for all actions
  243. *
  244. * @return void
  245. * @access public
  246. */
  247. function requireAuth() {
  248. $args = func_get_args();
  249. $this->_requireMethod('Auth', $args);
  250. }
  251. /**
  252. * Sets the actions that require an HTTP-authenticated request, or empty for all actions
  253. *
  254. * @return void
  255. * @access public
  256. */
  257. function requireLogin() {
  258. $args = func_get_args();
  259. $base = $this->loginOptions;
  260. foreach ($args as $i => $arg) {
  261. if (is_array($arg)) {
  262. $this->loginOptions = $arg;
  263. unset($args[$i]);
  264. }
  265. }
  266. $this->loginOptions = array_merge($base, $this->loginOptions);
  267. $this->_requireMethod('Login', $args);
  268. if (isset($this->loginOptions['users'])) {
  269. $this->loginUsers =& $this->loginOptions['users'];
  270. }
  271. }
  272. /**
  273. * Attempts to validate the login credentials for an HTTP-authenticated request
  274. *
  275. * @param string $type Either 'basic', 'digest', or null. If null/empty, will try both.
  276. * @return mixed If successful, returns an array with login name and password, otherwise null.
  277. * @access public
  278. */
  279. function loginCredentials($type = null) {
  280. switch (strtolower($type)) {
  281. case 'basic':
  282. $login = array('username' => env('PHP_AUTH_USER'), 'password' => env('PHP_AUTH_PW'));
  283. if (!empty($login['username'])) {
  284. return $login;
  285. }
  286. break;
  287. case 'digest':
  288. default:
  289. $digest = null;
  290. if (version_compare(PHP_VERSION, '5.1') != -1) {
  291. $digest = env('PHP_AUTH_DIGEST');
  292. } elseif (function_exists('apache_request_headers')) {
  293. $headers = apache_request_headers();
  294. if (isset($headers['Authorization']) && !empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) == 'Digest ') {
  295. $digest = substr($headers['Authorization'], 7);
  296. }
  297. } else {
  298. // Server doesn't support digest-auth headers
  299. trigger_error(__('SecurityComponent::loginCredentials() - Server does not support digest authentication', true), E_USER_WARNING);
  300. }
  301. if (!empty($digest)) {
  302. return $this->parseDigestAuthData($digest);
  303. }
  304. break;
  305. }
  306. return null;
  307. }
  308. /**
  309. * Generates the text of an HTTP-authentication request header from an array of options.
  310. *
  311. * @param array $options Set of options for header
  312. * @return string HTTP-authentication request header
  313. * @access public
  314. */
  315. function loginRequest($options = array()) {
  316. $options = array_merge($this->loginOptions, $options);
  317. $this->_setLoginDefaults($options);
  318. $auth = 'WWW-Authenticate: ' . ucfirst($options['type']);
  319. $out = array('realm="' . $options['realm'] . '"');
  320. if (strtolower($options['type']) == 'digest') {
  321. $out[] = 'qop="auth"';
  322. $out[] = 'nonce="' . uniqid("") . '"';
  323. $out[] = 'opaque="' . md5($options['realm']) . '"';
  324. }
  325. return $auth . ' ' . implode(',', $out);
  326. }
  327. /**
  328. * Parses an HTTP digest authentication response, and returns an array of the data, or null on failure.
  329. *
  330. * @param string $digest Digest authentication response
  331. * @return array Digest authentication parameters
  332. * @access public
  333. */
  334. function parseDigestAuthData($digest) {
  335. if (substr($digest, 0, 7) == 'Digest ') {
  336. $digest = substr($digest, 7);
  337. }
  338. $keys = array();
  339. $match = array();
  340. $req = array('nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1);
  341. preg_match_all('@(\w+)=([\'"]?)([a-zA-Z0-9=./\_-]+)\2@', $digest, $match, PREG_SET_ORDER);
  342. foreach ($match as $i) {
  343. $keys[$i[1]] = $i[3];
  344. unset($req[$i[1]]);
  345. }
  346. if (empty($req)) {
  347. return $keys;
  348. }
  349. return null;
  350. }
  351. /**
  352. * Generates a hash to be compared with an HTTP digest-authenticated response
  353. *
  354. * @param array $data HTTP digest response data, as parsed by SecurityComponent::parseDigestAuthData()
  355. * @return string Digest authentication hash
  356. * @access public
  357. * @see SecurityComponent::parseDigestAuthData()
  358. */
  359. function generateDigestResponseHash($data) {
  360. return md5(
  361. md5($data['username'] . ':' . $this->loginOptions['realm'] . ':' . $this->loginUsers[$data['username']]) .
  362. ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' .
  363. md5(env('REQUEST_METHOD') . ':' . $data['uri'])
  364. );
  365. }
  366. /**
  367. * Black-hole an invalid request with a 404 error or custom callback. If SecurityComponent::$blackHoleCallback
  368. * is specified, it will use this callback by executing the method indicated in $error
  369. *
  370. * @param object $controller Instantiating controller
  371. * @param string $error Error method
  372. * @return mixed If specified, controller blackHoleCallback's response, or no return otherwise
  373. * @access public
  374. * @see SecurityComponent::$blackHoleCallback
  375. */
  376. function blackHole(&$controller, $error = '') {
  377. $this->Session->delete('_Token');
  378. if ($this->blackHoleCallback == null) {
  379. $code = 404;
  380. if ($error == 'login') {
  381. $code = 401;
  382. $controller->header($this->loginRequest());
  383. }
  384. $controller->redirect(null, $code, true);
  385. } else {
  386. return $this->_callback($controller, $this->blackHoleCallback, array($error));
  387. }
  388. }
  389. /**
  390. * Sets the actions that require a $method HTTP request, or empty for all actions
  391. *
  392. * @param string $method The HTTP method to assign controller actions to
  393. * @param array $actions Controller actions to set the required HTTP method to.
  394. * @return void
  395. * @access protected
  396. */
  397. function _requireMethod($method, $actions = array()) {
  398. if (isset($actions[0]) && is_array($actions[0])) {
  399. $actions = $actions[0];
  400. }
  401. $this->{'require' . $method} = (empty($actions)) ? array('*'): $actions;
  402. }
  403. /**
  404. * Check if HTTP methods are required
  405. *
  406. * @param object $controller Instantiating controller
  407. * @return bool true if $method is required
  408. * @access protected
  409. */
  410. function _methodsRequired(&$controller) {
  411. foreach (array('Post', 'Get', 'Put', 'Delete') as $method) {
  412. $property = 'require' . $method;
  413. if (is_array($this->$property) && !empty($this->$property)) {
  414. $require = array_map('strtolower', $this->$property);
  415. if (in_array($this->_action, $require) || $this->$property == array('*')) {
  416. if (!$this->RequestHandler->{'is' . $method}()) {
  417. if (!$this->blackHole($controller, strtolower($method))) {
  418. return null;
  419. }
  420. }
  421. }
  422. }
  423. }
  424. return true;
  425. }
  426. /**
  427. * Check if access requires secure connection
  428. *
  429. * @param object $controller Instantiating controller
  430. * @return bool true if secure connection required
  431. * @access protected
  432. */
  433. function _secureRequired(&$controller) {
  434. if (is_array($this->requireSecure) && !empty($this->requireSecure)) {
  435. $requireSecure = array_map('strtolower', $this->requireSecure);
  436. if (in_array($this->_action, $requireSecure) || $this->requireSecure == array('*')) {
  437. if (!$this->RequestHandler->isSSL()) {
  438. if (!$this->blackHole($controller, 'secure')) {
  439. return null;
  440. }
  441. }
  442. }
  443. }
  444. return true;
  445. }
  446. /**
  447. * Check if authentication is required
  448. *
  449. * @param object $controller Instantiating controller
  450. * @return bool true if authentication required
  451. * @access protected
  452. */
  453. function _authRequired(&$controller) {
  454. if (is_array($this->requireAuth) && !empty($this->requireAuth) && !empty($controller->data)) {
  455. $requireAuth = array_map('strtolower', $this->requireAuth);
  456. if (in_array($this->_action, $requireAuth) || $this->requireAuth == array('*')) {
  457. if (!isset($controller->data['_Token'] )) {
  458. if (!$this->blackHole($controller, 'auth')) {
  459. return null;
  460. }
  461. }
  462. if ($this->Session->check('_Token')) {
  463. $tData = unserialize($this->Session->read('_Token'));
  464. if (!empty($tData['allowedControllers']) && !in_array($controller->params['controller'], $tData['allowedControllers']) || !empty($tData['allowedActions']) && !in_array($controller->params['action'], $tData['allowedActions'])) {
  465. if (!$this->blackHole($controller, 'auth')) {
  466. return null;
  467. }
  468. }
  469. } else {
  470. if (!$this->blackHole($controller, 'auth')) {
  471. return null;
  472. }
  473. }
  474. }
  475. }
  476. return true;
  477. }
  478. /**
  479. * Check if login is required
  480. *
  481. * @param object $controller Instantiating controller
  482. * @return bool true if login is required
  483. * @access protected
  484. */
  485. function _loginRequired(&$controller) {
  486. if (is_array($this->requireLogin) && !empty($this->requireLogin)) {
  487. $requireLogin = array_map('strtolower', $this->requireLogin);
  488. if (in_array($this->_action, $requireLogin) || $this->requireLogin == array('*')) {
  489. $login = $this->loginCredentials($this->loginOptions['type']);
  490. if ($login == null) {
  491. $controller->header($this->loginRequest());
  492. if (!empty($this->loginOptions['prompt'])) {
  493. $this->_callback($controller, $this->loginOptions['prompt']);
  494. } else {
  495. $this->blackHole($controller, 'login');
  496. }
  497. } else {
  498. if (isset($this->loginOptions['login'])) {
  499. $this->_callback($controller, $this->loginOptions['login'], array($login));
  500. } else {
  501. if (strtolower($this->loginOptions['type']) == 'digest') {
  502. if ($login && isset($this->loginUsers[$login['username']])) {
  503. if ($login['response'] == $this->generateDigestResponseHash($login)) {
  504. return true;
  505. }
  506. }
  507. $this->blackHole($controller, 'login');
  508. } else {
  509. if (
  510. !(in_array($login['username'], array_keys($this->loginUsers)) &&
  511. $this->loginUsers[$login['username']] == $login['password'])
  512. ) {
  513. $this->blackHole($controller, 'login');
  514. }
  515. }
  516. }
  517. }
  518. }
  519. }
  520. return true;
  521. }
  522. /**
  523. * Validate submitted form
  524. *
  525. * @param object $controller Instantiating controller
  526. * @return bool true if submitted form is valid
  527. * @access protected
  528. */
  529. function _validatePost(&$controller) {
  530. if (empty($controller->data)) {
  531. return true;
  532. }
  533. $data = $controller->data;
  534. if (!isset($data['_Token']) || !isset($data['_Token']['fields']) || !isset($data['_Token']['key'])) {
  535. return false;
  536. }
  537. $token = $data['_Token']['key'];
  538. if ($this->Session->check('_Token')) {
  539. $tokenData = unserialize($this->Session->read('_Token'));
  540. if ($tokenData['expires'] < time() || $tokenData['key'] !== $token) {
  541. return false;
  542. }
  543. }
  544. $locked = null;
  545. $check = $controller->data;
  546. $token = urldecode($check['_Token']['fields']);
  547. if (strpos($token, ':')) {
  548. list($token, $locked) = explode(':', $token, 2);
  549. }
  550. unset($check['_Token']);
  551. $lockedFields = array();
  552. $fields = Set::flatten($check);
  553. $fieldList = array_keys($fields);
  554. $locked = unserialize(str_rot13($locked));
  555. $multi = array();
  556. foreach ($fieldList as $i => $key) {
  557. if (preg_match('/\.\d+$/', $key)) {
  558. $multi[$i] = preg_replace('/\.\d+$/', '', $key);
  559. unset($fieldList[$i]);
  560. }
  561. }
  562. if (!empty($multi)) {
  563. $fieldList += array_unique($multi);
  564. }
  565. foreach ($fieldList as $i => $key) {
  566. $isDisabled = false;
  567. $isLocked = (is_array($locked) && in_array($key, $locked));
  568. if (!empty($this->disabledFields)) {
  569. foreach ((array)$this->disabledFields as $disabled) {
  570. $disabled = explode('.', $disabled);
  571. $field = array_values(array_intersect(explode('.', $key), $disabled));
  572. $isDisabled = ($field === $disabled);
  573. if ($isDisabled) {
  574. break;
  575. }
  576. }
  577. }
  578. if ($isDisabled || $isLocked) {
  579. unset($fieldList[$i]);
  580. if ($isLocked) {
  581. $lockedFields[$key] = $fields[$key];
  582. }
  583. }
  584. }
  585. sort($fieldList, SORT_STRING);
  586. ksort($lockedFields, SORT_STRING);
  587. $fieldList += $lockedFields;
  588. $check = Security::hash(serialize($fieldList) . Configure::read('Security.salt'));
  589. return ($token === $check);
  590. }
  591. /**
  592. * Add authentication key for new form posts
  593. *
  594. * @param object $controller Instantiating controller
  595. * @return bool Success
  596. * @access protected
  597. */
  598. function _generateToken(&$controller) {
  599. if (isset($controller->params['requested']) && $controller->params['requested'] === 1) {
  600. if ($this->Session->check('_Token')) {
  601. $tokenData = unserialize($this->Session->read('_Token'));
  602. $controller->params['_Token'] = $tokenData;
  603. }
  604. return false;
  605. }
  606. $authKey = Security::generateAuthKey();
  607. $expires = strtotime('+' . Security::inactiveMins() . ' minutes');
  608. $token = array(
  609. 'key' => $authKey,
  610. 'expires' => $expires,
  611. 'allowedControllers' => $this->allowedControllers,
  612. 'allowedActions' => $this->allowedActions,
  613. 'disabledFields' => $this->disabledFields
  614. );
  615. if (!isset($controller->data)) {
  616. $controller->data = array();
  617. }
  618. if ($this->Session->check('_Token')) {
  619. $tokenData = unserialize($this->Session->read('_Token'));
  620. $valid = (
  621. isset($tokenData['expires']) &&
  622. $tokenData['expires'] > time() &&
  623. isset($tokenData['key'])
  624. );
  625. if ($valid) {
  626. $token['key'] = $tokenData['key'];
  627. }
  628. }
  629. $controller->params['_Token'] = $token;
  630. $this->Session->write('_Token', serialize($token));
  631. return true;
  632. }
  633. /**
  634. * Sets the default login options for an HTTP-authenticated request
  635. *
  636. * @param array $options Default login options
  637. * @return void
  638. * @access protected
  639. */
  640. function _setLoginDefaults(&$options) {
  641. $options = array_merge(array(
  642. 'type' => 'basic',
  643. 'realm' => env('SERVER_NAME'),
  644. 'qop' => 'auth',
  645. 'nonce' => String::uuid()
  646. ), array_filter($options));
  647. $options = array_merge(array('opaque' => md5($options['realm'])), $options);
  648. }
  649. /**
  650. * Calls a controller callback method
  651. *
  652. * @param object $controller Controller to run callback on
  653. * @param string $method Method to execute
  654. * @param array $params Parameters to send to method
  655. * @return mixed Controller callback method's response
  656. * @access protected
  657. */
  658. function _callback(&$controller, $method, $params = array()) {
  659. if (is_callable(array($controller, $method))) {
  660. return call_user_func_array(array(&$controller, $method), empty($params) ? null : $params);
  661. } else {
  662. return null;
  663. }
  664. }
  665. }
  666. ?>