security.php

Go to the documentation of this file.
00001 <?php
00002 /* SVN FILE: $Id: security.php 8282 2009-08-03 19:40:02Z jperras $ */
00003 /**
00004  * Short description for file.
00005  *
00006  * Long description for file
00007  *
00008  * PHP versions 4 and 5
00009  *
00010  * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org)
00011  * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
00012  *
00013  * Licensed under The MIT License
00014  * Redistributions of files must retain the above copyright notice.
00015  *
00016  * @filesource
00017  * @copyright     Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
00018  * @link          http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
00019  * @package       cake
00020  * @subpackage    cake.cake.libs.controller.components
00021  * @since         CakePHP(tm) v 0.10.8.2156
00022  * @version       $Revision: 8282 $
00023  * @modifiedby    $LastChangedBy: jperras $
00024  * @lastmodified  $Date: 2009-08-03 15:40:02 -0400 (Mon, 03 Aug 2009) $
00025  * @license       http://www.opensource.org/licenses/mit-license.php The MIT License
00026  */
00027 /**
00028  * Short description for file.
00029  *
00030  * Long description for file
00031  *
00032  * @package       cake
00033  * @subpackage    cake.cake.libs.controller.components
00034  */
00035 class SecurityComponent extends Object {
00036 /**
00037  * The controller method that will be called if this request is black-hole'd
00038  *
00039  * @var string
00040  * @access public
00041  */
00042     var $blackHoleCallback = null;
00043 /**
00044  * List of controller actions for which a POST request is required
00045  *
00046  * @var array
00047  * @access public
00048  * @see SecurityComponent::requirePost()
00049  */
00050     var $requirePost = array();
00051 /**
00052  * List of controller actions for which a GET request is required
00053  *
00054  * @var array
00055  * @access public
00056  * @see SecurityComponent::requireGet()
00057  */
00058     var $requireGet = array();
00059 /**
00060  * List of controller actions for which a PUT request is required
00061  *
00062  * @var array
00063  * @access public
00064  * @see SecurityComponent::requirePut()
00065  */
00066     var $requirePut = array();
00067 /**
00068  * List of controller actions for which a DELETE request is required
00069  *
00070  * @var array
00071  * @access public
00072  * @see SecurityComponent::requireDelete()
00073  */
00074     var $requireDelete = array();
00075 /**
00076  * List of actions that require an SSL-secured connection
00077  *
00078  * @var array
00079  * @access public
00080  * @see SecurityComponent::requireSecure()
00081  */
00082     var $requireSecure = array();
00083 /**
00084  * List of actions that require a valid authentication key
00085  *
00086  * @var array
00087  * @access public
00088  * @see SecurityComponent::requireAuth()
00089  */
00090     var $requireAuth = array();
00091 /**
00092  * List of actions that require an HTTP-authenticated login (basic or digest)
00093  *
00094  * @var array
00095  * @access public
00096  * @see SecurityComponent::requireLogin()
00097  */
00098     var $requireLogin = array();
00099 /**
00100  * Login options for SecurityComponent::requireLogin()
00101  *
00102  * @var array
00103  * @access public
00104  * @see SecurityComponent::requireLogin()
00105  */
00106     var $loginOptions = array('type' => '', 'prompt' => null);
00107 /**
00108  * An associative array of usernames/passwords used for HTTP-authenticated logins.
00109  * If using digest authentication, passwords should be MD5-hashed.
00110  *
00111  * @var array
00112  * @access public
00113  * @see SecurityComponent::requireLogin()
00114  */
00115     var $loginUsers = array();
00116 /**
00117  * Controllers from which actions of the current controller are allowed to receive
00118  * requests.
00119  *
00120  * @var array
00121  * @access public
00122  * @see SecurityComponent::requireAuth()
00123  */
00124     var $allowedControllers = array();
00125 /**
00126  * Actions from which actions of the current controller are allowed to receive
00127  * requests.
00128  *
00129  * @var array
00130  * @access public
00131  * @see SecurityComponent::requireAuth()
00132  */
00133     var $allowedActions = array();
00134 /**
00135  * Form fields to disable
00136  *
00137  * @var array
00138  * @access public
00139  */
00140     var $disabledFields = array();
00141 /**
00142  * Whether to validate POST data.  Set to false to disable for data coming from 3rd party
00143  * services, etc.
00144  *
00145  * @var boolean
00146  * @access public
00147  */
00148     var $validatePost = true;
00149 /**
00150  * Other components used by the Security component
00151  *
00152  * @var array
00153  * @access public
00154  */
00155     var $components = array('RequestHandler', 'Session');
00156 /**
00157  * Holds the current action of the controller
00158  *
00159  * @var string
00160  */
00161     var $_action = null;
00162 /**
00163  * Component startup. All security checking happens here.
00164  *
00165  * @param object $controller Instantiating controller
00166  * @access public
00167  */
00168     function startup(&$controller) {
00169         $this->_action = strtolower($controller->action);
00170         $this->_methodsRequired($controller);
00171         $this->_secureRequired($controller);
00172         $this->_authRequired($controller);
00173         $this->_loginRequired($controller);
00174 
00175         $isPost = ($this->RequestHandler->isPost() || $this->RequestHandler->isPut());
00176         $isRequestAction = (
00177             !isset($controller->params['requested']) ||
00178             $controller->params['requested'] != 1
00179         );
00180 
00181         if ($isPost && $isRequestAction && $this->validatePost) {
00182             if ($this->_validatePost($controller) === false) {
00183                 if (!$this->blackHole($controller, 'auth')) {
00184                     return null;
00185                 }
00186             }
00187         }
00188         $this->_generateToken($controller);
00189     }
00190 /**
00191  * Sets the actions that require a POST request, or empty for all actions
00192  *
00193  * @return void
00194  * @access public
00195  */
00196     function requirePost() {
00197         $args = func_get_args();
00198         $this->_requireMethod('Post', $args);
00199     }
00200 /**
00201  * Sets the actions that require a GET request, or empty for all actions
00202  *
00203  * @return void
00204  * @access public
00205  */
00206     function requireGet() {
00207         $args = func_get_args();
00208         $this->_requireMethod('Get', $args);
00209     }
00210 /**
00211  * Sets the actions that require a PUT request, or empty for all actions
00212  *
00213  * @return void
00214  * @access public
00215  */
00216     function requirePut() {
00217         $args = func_get_args();
00218         $this->_requireMethod('Put', $args);
00219     }
00220 /**
00221  * Sets the actions that require a DELETE request, or empty for all actions
00222  *
00223  * @return void
00224  * @access public
00225  */
00226     function requireDelete() {
00227         $args = func_get_args();
00228         $this->_requireMethod('Delete', $args);
00229     }
00230 /**
00231  * Sets the actions that require a request that is SSL-secured, or empty for all actions
00232  *
00233  * @return void
00234  * @access public
00235  */
00236     function requireSecure() {
00237         $args = func_get_args();
00238         $this->_requireMethod('Secure', $args);
00239     }
00240 /**
00241  * Sets the actions that require an authenticated request, or empty for all actions
00242  *
00243  * @return void
00244  * @access public
00245  */
00246     function requireAuth() {
00247         $args = func_get_args();
00248         $this->_requireMethod('Auth', $args);
00249     }
00250 /**
00251  * Sets the actions that require an HTTP-authenticated request, or empty for all actions
00252  *
00253  * @return void
00254  * @access public
00255  */
00256     function requireLogin() {
00257         $args = func_get_args();
00258         $base = $this->loginOptions;
00259 
00260         foreach ($args as $i => $arg) {
00261             if (is_array($arg)) {
00262                 $this->loginOptions = $arg;
00263                 unset($args[$i]);
00264             }
00265         }
00266         $this->loginOptions = array_merge($base, $this->loginOptions);
00267         $this->_requireMethod('Login', $args);
00268 
00269         if (isset($this->loginOptions['users'])) {
00270             $this->loginUsers =& $this->loginOptions['users'];
00271         }
00272     }
00273 /**
00274  * Attempts to validate the login credentials for an HTTP-authenticated request
00275  *
00276  * @param string $type Either 'basic', 'digest', or null. If null/empty, will try both.
00277  * @return mixed If successful, returns an array with login name and password, otherwise null.
00278  * @access public
00279  */
00280     function loginCredentials($type = null) {
00281         switch (strtolower($type)) {
00282             case 'basic':
00283                 $login = array('username' => env('PHP_AUTH_USER'), 'password' => env('PHP_AUTH_PW'));
00284                 if (!empty($login['username'])) {
00285                     return $login;
00286                 }
00287             break;
00288             case 'digest':
00289             default:
00290                 $digest = null;
00291 
00292                 if (version_compare(PHP_VERSION, '5.1') != -1) {
00293                     $digest = env('PHP_AUTH_DIGEST');
00294                 } elseif (function_exists('apache_request_headers')) {
00295                     $headers = apache_request_headers();
00296                     if (isset($headers['Authorization']) && !empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) == 'Digest ') {
00297                         $digest = substr($headers['Authorization'], 7);
00298                     }
00299                 } else {
00300                     // Server doesn't support digest-auth headers
00301                     trigger_error(__('SecurityComponent::loginCredentials() - Server does not support digest authentication', true), E_USER_WARNING);
00302                 }
00303 
00304                 if (!empty($digest)) {
00305                     return $this->parseDigestAuthData($digest);
00306                 }
00307             break;
00308         }
00309         return null;
00310     }
00311 /**
00312  * Generates the text of an HTTP-authentication request header from an array of options.
00313  *
00314  * @param array $options Set of options for header
00315  * @return string HTTP-authentication request header
00316  * @access public
00317  */
00318     function loginRequest($options = array()) {
00319         $options = array_merge($this->loginOptions, $options);
00320         $this->_setLoginDefaults($options);
00321         $auth = 'WWW-Authenticate: ' . ucfirst($options['type']);
00322         $out = array('realm="' . $options['realm'] . '"');
00323 
00324         if (strtolower($options['type']) == 'digest') {
00325             $out[] = 'qop="auth"';
00326             $out[] = 'nonce="' . uniqid("") . '"';
00327             $out[] = 'opaque="' . md5($options['realm']).'"';
00328         }
00329 
00330         return $auth . ' ' . join(',', $out);
00331     }
00332 /**
00333  * Parses an HTTP digest authentication response, and returns an array of the data, or null on failure.
00334  *
00335  * @param string $digest Digest authentication response
00336  * @return array Digest authentication parameters
00337  * @access public
00338  */
00339     function parseDigestAuthData($digest) {
00340         if (substr($digest, 0, 7) == 'Digest ') {
00341             $digest = substr($digest, 7);
00342         }
00343         $keys = array();
00344         $match = array();
00345         $req = array('nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1);
00346         preg_match_all('@(\w+)=([\'"]?)([a-zA-Z0-9=./\_-]+)\2@', $digest, $match, PREG_SET_ORDER);
00347 
00348         foreach ($match as $i) {
00349             $keys[$i[1]] = $i[3];
00350             unset($req[$i[1]]);
00351         }
00352 
00353         if (empty($req)) {
00354             return $keys;
00355         }
00356         return null;
00357     }
00358 /**
00359  * Generates a hash to be compared with an HTTP digest-authenticated response
00360  *
00361  * @param array $data HTTP digest response data, as parsed by SecurityComponent::parseDigestAuthData()
00362  * @return string Digest authentication hash
00363  * @access public
00364  * @see SecurityComponent::parseDigestAuthData()
00365  */
00366     function generateDigestResponseHash($data) {
00367         return md5(
00368             md5($data['username'] . ':' . $this->loginOptions['realm'] . ':' . $this->loginUsers[$data['username']]) .
00369             ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' .
00370             md5(env('REQUEST_METHOD') . ':' . $data['uri'])
00371         );
00372     }
00373 /**
00374  * Black-hole an invalid request with a 404 error or custom callback. If SecurityComponent::$blackHoleCallback
00375  * is specified, it will use this callback by executing the method indicated in $error
00376  *
00377  * @param object $controller Instantiating controller
00378  * @param string $error Error method
00379  * @return mixed If specified, controller blackHoleCallback's response, or no return otherwise
00380  * @access public
00381  * @see SecurityComponent::$blackHoleCallback
00382  */
00383     function blackHole(&$controller, $error = '') {
00384         $this->Session->del('_Token');
00385 
00386         if ($this->blackHoleCallback == null) {
00387             $code = 404;
00388             if ($error == 'login') {
00389                 $code = 401;
00390                 $controller->header($this->loginRequest());
00391             }
00392             $controller->redirect(null, $code, true);
00393         } else {
00394             return $this->_callback($controller, $this->blackHoleCallback, array($error));
00395         }
00396     }
00397 /**
00398  * Sets the actions that require a $method HTTP request, or empty for all actions
00399  *
00400  * @param string $method The HTTP method to assign controller actions to
00401  * @param array $actions Controller actions to set the required HTTP method to.
00402  * @return void
00403  * @access protected
00404  */
00405     function _requireMethod($method, $actions = array()) {
00406         $this->{'require' . $method} = (empty($actions)) ? array('*'): $actions;
00407     }
00408 /**
00409  * Check if HTTP methods are required
00410  *
00411  * @param object $controller Instantiating controller
00412  * @return bool true if $method is required
00413  * @access protected
00414  */
00415     function _methodsRequired(&$controller) {
00416         foreach (array('Post', 'Get', 'Put', 'Delete') as $method) {
00417             $property = 'require' . $method;
00418             if (is_array($this->$property) && !empty($this->$property)) {
00419                 $require = array_map('strtolower', $this->$property);
00420 
00421                 if (in_array($this->_action, $require) || $this->$property == array('*')) {
00422                     if (!$this->RequestHandler->{'is' . $method}()) {
00423                         if (!$this->blackHole($controller, strtolower($method))) {
00424                             return null;
00425                         }
00426                     }
00427                 }
00428             }
00429         }
00430         return true;
00431     }
00432 /**
00433  * Check if access requires secure connection
00434  *
00435  * @param object $controller Instantiating controller
00436  * @return bool true if secure connection required
00437  * @access protected
00438  */
00439     function _secureRequired(&$controller) {
00440         if (is_array($this->requireSecure) && !empty($this->requireSecure)) {
00441             $requireSecure = array_map('strtolower', $this->requireSecure);
00442 
00443             if (in_array($this->_action, $requireSecure) || $this->requireSecure == array('*')) {
00444                 if (!$this->RequestHandler->isSSL()) {
00445                     if (!$this->blackHole($controller, 'secure')) {
00446                         return null;
00447                     }
00448                 }
00449             }
00450         }
00451         return true;
00452     }
00453 /**
00454  * Check if authentication is required
00455  *
00456  * @param object $controller Instantiating controller
00457  * @return bool true if authentication required
00458  * @access protected
00459  */
00460     function _authRequired(&$controller) {
00461         if (is_array($this->requireAuth) && !empty($this->requireAuth) && !empty($controller->data)) {
00462             $requireAuth = array_map('strtolower', $this->requireAuth);
00463 
00464             if (in_array($this->_action, $requireAuth) || $this->requireAuth == array('*')) {
00465                 if (!isset($controller->data['_Token'] )) {
00466                     if (!$this->blackHole($controller, 'auth')) {
00467                         return null;
00468                     }
00469                 }
00470 
00471                 if ($this->Session->check('_Token')) {
00472                     $tData = unserialize($this->Session->read('_Token'));
00473 
00474                     if (!empty($tData['allowedControllers']) && !in_array($controller->params['controller'], $tData['allowedControllers']) || !empty($tData['allowedActions']) && !in_array($controller->params['action'], $tData['allowedActions'])) {
00475                         if (!$this->blackHole($controller, 'auth')) {
00476                             return null;
00477                         }
00478                     }
00479                 } else {
00480                     if (!$this->blackHole($controller, 'auth')) {
00481                         return null;
00482                     }
00483                 }
00484             }
00485         }
00486         return true;
00487     }
00488 /**
00489  * Check if login is required
00490  *
00491  * @param object $controller Instantiating controller
00492  * @return bool true if login is required
00493  * @access protected
00494  */
00495     function _loginRequired(&$controller) {
00496         if (is_array($this->requireLogin) && !empty($this->requireLogin)) {
00497             $requireLogin = array_map('strtolower', $this->requireLogin);
00498 
00499             if (in_array($this->_action, $requireLogin) || $this->requireLogin == array('*')) {
00500                 $login = $this->loginCredentials($this->loginOptions['type']);
00501 
00502                 if ($login == null) {
00503                     $controller->header($this->loginRequest());
00504 
00505                     if (!empty($this->loginOptions['prompt'])) {
00506                         $this->_callback($controller, $this->loginOptions['prompt']);
00507                     } else {
00508                         $this->blackHole($controller, 'login');
00509                     }
00510                 } else {
00511                     if (isset($this->loginOptions['login'])) {
00512                         $this->_callback($controller, $this->loginOptions['login'], array($login));
00513                     } else {
00514                         if (strtolower($this->loginOptions['type']) == 'digest') {
00515                             if ($login && isset($this->loginUsers[$login['username']])) {
00516                                 if ($login['response'] == $this->generateDigestResponseHash($login)) {
00517                                     return true;
00518                                 }
00519                             }
00520                             $this->blackHole($controller, 'login');
00521                         } else {
00522                             if (
00523                                 !(in_array($login['username'], array_keys($this->loginUsers)) &&
00524                                 $this->loginUsers[$login['username']] == $login['password'])
00525                             ) {
00526                                 $this->blackHole($controller, 'login');
00527                             }
00528                         }
00529                     }
00530                 }
00531             }
00532         }
00533         return true;
00534     }
00535 /**
00536  * Validate submitted form
00537  *
00538  * @param object $controller Instantiating controller
00539  * @return bool true if submitted form is valid
00540  * @access protected
00541  */
00542     function _validatePost(&$controller) {
00543         if (empty($controller->data)) {
00544             return true;
00545         }
00546         $data = $controller->data;
00547 
00548         if (!isset($data['_Token']) || !isset($data['_Token']['fields'])) {
00549             return false;
00550         }
00551         $token = $data['_Token']['key'];
00552 
00553         if ($this->Session->check('_Token')) {
00554             $tokenData = unserialize($this->Session->read('_Token'));
00555 
00556             if ($tokenData['expires'] < time() || $tokenData['key'] !== $token) {
00557                 return false;
00558             }
00559         }
00560 
00561         $locked = null;
00562         $check = $controller->data;
00563         $token = urldecode($check['_Token']['fields']);
00564 
00565         if (strpos($token, ':')) {
00566             list($token, $locked) = explode(':', $token, 2);
00567         }
00568         unset($check['_Token']);
00569 
00570         $lockedFields = array();
00571         $fields = Set::flatten($check);
00572         $fieldList = array_keys($fields);
00573         $locked = unserialize(str_rot13($locked));
00574         $multi = array();
00575 
00576         foreach ($fieldList as $i => $key) {
00577             if (preg_match('/\.\d+$/', $key)) {
00578                 $multi[$i] = preg_replace('/\.\d+$/', '', $key);
00579                 unset($fieldList[$i]);
00580             }
00581         }
00582         if (!empty($multi)) {
00583             $fieldList += array_unique($multi);
00584         }
00585 
00586         foreach ($fieldList as $i => $key) {
00587             $isDisabled = false;
00588             $isLocked = (is_array($locked) && in_array($key, $locked));
00589 
00590             if (!empty($this->disabledFields)) {
00591                 foreach ((array)$this->disabledFields as $disabled) {
00592                     $disabled = explode('.', $disabled);
00593                     $field = array_values(array_intersect(explode('.', $key), $disabled));
00594                     $isDisabled = ($field === $disabled);
00595                     if ($isDisabled) {
00596                         break;
00597                     }
00598                 }
00599             }
00600 
00601             if ($isDisabled || $isLocked) {
00602                 unset($fieldList[$i]);
00603                 if ($isLocked) {
00604                     $lockedFields[$key] = $fields[$key];
00605                 }
00606             }
00607         }
00608         sort($fieldList, SORT_STRING);
00609         ksort($lockedFields, SORT_STRING);
00610 
00611         $fieldList += $lockedFields;
00612         $check = Security::hash(serialize($fieldList) . Configure::read('Security.salt'));
00613         return ($token === $check);
00614     }
00615 /**
00616  * Add authentication key for new form posts
00617  *
00618  * @param object $controller Instantiating controller
00619  * @return bool Success
00620  * @access protected
00621  */
00622     function _generateToken(&$controller) {
00623         if (isset($controller->params['requested']) && $controller->params['requested'] === 1) {
00624             return false;
00625         }
00626         $authKey = Security::generateAuthKey();
00627         $expires = strtotime('+' . Security::inactiveMins() . ' minutes');
00628         $token = array(
00629             'key' => $authKey,
00630             'expires' => $expires,
00631             'allowedControllers' => $this->allowedControllers,
00632             'allowedActions' => $this->allowedActions,
00633             'disabledFields' => $this->disabledFields
00634         );
00635 
00636         if (!isset($controller->data)) {
00637             $controller->data = array();
00638         }
00639 
00640         if ($this->Session->check('_Token')) {
00641             $tokenData = unserialize($this->Session->read('_Token'));
00642             $valid = (
00643                 isset($tokenData['expires']) &&
00644                 $tokenData['expires'] > time() &&
00645                 isset($tokenData['key'])
00646             );
00647 
00648             if ($valid) {
00649                 $token['key'] = $tokenData['key'];
00650             }
00651         }
00652         $controller->params['_Token'] = $token;
00653         $this->Session->write('_Token', serialize($token));
00654 
00655         return true;
00656     }
00657 /**
00658  * Sets the default login options for an HTTP-authenticated request
00659  *
00660  * @param array $options Default login options
00661  * @return void
00662  * @access protected
00663  */
00664     function _setLoginDefaults(&$options) {
00665         $options = array_merge(array(
00666             'type' => 'basic',
00667             'realm' => env('SERVER_NAME'),
00668             'qop' => 'auth',
00669             'nonce' => String::uuid()
00670         ), array_filter($options));
00671         $options = array_merge(array('opaque' => md5($options['realm'])), $options);
00672     }
00673 /**
00674  * Calls a controller callback method
00675  *
00676  * @param object $controller Controller to run callback on
00677  * @param string $method Method to execute
00678  * @param array $params Parameters to send to method
00679  * @return mixed Controller callback method's response
00680  * @access protected
00681  */
00682     function _callback(&$controller, $method, $params = array()) {
00683         if (is_callable(array($controller, $method))) {
00684             return call_user_func_array(array(&$controller, $method), empty($params) ? null : $params);
00685         } else {
00686             // Debug::warning('Callback method ' . $method . ' in controller ' . get_class($controller)
00687             return null;
00688         }
00689     }
00690 }
00691 
00692 ?>

Generated on Sun Nov 22 00:30:53 2009 for CakePHP 1.2.x.x (v1.2.4.8284) by doxygen 1.4.7