PageRenderTime 42ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/code/web/public_php/webtt/cake/libs/controller/components/security.php

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