'', 'prompt' => null); /** * An associative array of usernames/passwords used for HTTP-authenticated logins. * If using digest authentication, passwords should be MD5-hashed. * * @var array * @access public * @see SecurityComponent::requireLogin() */ var $loginUsers = array(); /** * Controllers from which actions of the current controller are allowed to receive * requests. * * @var array * @access public * @see SecurityComponent::requireAuth() */ var $allowedControllers = array(); /** * Actions from which actions of the current controller are allowed to receive * requests. * * @var array * @access public * @see SecurityComponent::requireAuth() */ var $allowedActions = array(); /** * Form fields to disable * * @var array * @access public */ var $disabledFields = array(); /** * Whether to validate POST data. Set to false to disable for data coming from 3rd party * services, etc. * * @var boolean * @access public */ var $validatePost = true; /** * Other components used by the Security component * * @var array * @access public */ var $components = array('RequestHandler', 'Session'); /** * Holds the current action of the controller * * @var string */ var $_action = null; /** * Component startup. All security checking happens here. * * @param object $controller Instantiating controller * @access public */ function startup(&$controller) { $this->_action = strtolower($controller->action); $this->_methodsRequired($controller); $this->_secureRequired($controller); $this->_authRequired($controller); $this->_loginRequired($controller); $isPost = ($this->RequestHandler->isPost() || $this->RequestHandler->isPut()); $isRequestAction = ( !isset($controller->params['requested']) || $controller->params['requested'] != 1 ); if ($isPost && $isRequestAction && $this->validatePost) { if ($this->_validatePost($controller) === false) { if (!$this->blackHole($controller, 'auth')) { return null; } } } $this->_generateToken($controller); } /** * Sets the actions that require a POST request, or empty for all actions * * @return void * @access public */ function requirePost() { $args = func_get_args(); $this->_requireMethod('Post', $args); } /** * Sets the actions that require a GET request, or empty for all actions * * @return void * @access public */ function requireGet() { $args = func_get_args(); $this->_requireMethod('Get', $args); } /** * Sets the actions that require a PUT request, or empty for all actions * * @return void * @access public */ function requirePut() { $args = func_get_args(); $this->_requireMethod('Put', $args); } /** * Sets the actions that require a DELETE request, or empty for all actions * * @return void * @access public */ function requireDelete() { $args = func_get_args(); $this->_requireMethod('Delete', $args); } /** * Sets the actions that require a request that is SSL-secured, or empty for all actions * * @return void * @access public */ function requireSecure() { $args = func_get_args(); $this->_requireMethod('Secure', $args); } /** * Sets the actions that require an authenticated request, or empty for all actions * * @return void * @access public */ function requireAuth() { $args = func_get_args(); $this->_requireMethod('Auth', $args); } /** * Sets the actions that require an HTTP-authenticated request, or empty for all actions * * @return void * @access public */ function requireLogin() { $args = func_get_args(); $base = $this->loginOptions; foreach ($args as $i => $arg) { if (is_array($arg)) { $this->loginOptions = $arg; unset($args[$i]); } } $this->loginOptions = array_merge($base, $this->loginOptions); $this->_requireMethod('Login', $args); if (isset($this->loginOptions['users'])) { $this->loginUsers =& $this->loginOptions['users']; } } /** * Attempts to validate the login credentials for an HTTP-authenticated request * * @param string $type Either 'basic', 'digest', or null. If null/empty, will try both. * @return mixed If successful, returns an array with login name and password, otherwise null. * @access public */ function loginCredentials($type = null) { switch (strtolower($type)) { case 'basic': $login = array('username' => env('PHP_AUTH_USER'), 'password' => env('PHP_AUTH_PW')); if (!empty($login['username'])) { return $login; } break; case 'digest': default: $digest = null; if (version_compare(PHP_VERSION, '5.1') != -1) { $digest = env('PHP_AUTH_DIGEST'); } elseif (function_exists('apache_request_headers')) { $headers = apache_request_headers(); if (isset($headers['Authorization']) && !empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) == 'Digest ') { $digest = substr($headers['Authorization'], 7); } } else { // Server doesn't support digest-auth headers trigger_error(__('SecurityComponent::loginCredentials() - Server does not support digest authentication', true), E_USER_WARNING); } if (!empty($digest)) { return $this->parseDigestAuthData($digest); } break; } return null; } /** * Generates the text of an HTTP-authentication request header from an array of options. * * @param array $options Set of options for header * @return string HTTP-authentication request header * @access public */ function loginRequest($options = array()) { $options = array_merge($this->loginOptions, $options); $this->_setLoginDefaults($options); $auth = 'WWW-Authenticate: ' . ucfirst($options['type']); $out = array('realm="' . $options['realm'] . '"'); if (strtolower($options['type']) == 'digest') { $out[] = 'qop="auth"'; $out[] = 'nonce="' . uniqid("") . '"'; $out[] = 'opaque="' . md5($options['realm']).'"'; } return $auth . ' ' . join(',', $out); } /** * Parses an HTTP digest authentication response, and returns an array of the data, or null on failure. * * @param string $digest Digest authentication response * @return array Digest authentication parameters * @access public */ function parseDigestAuthData($digest) { if (substr($digest, 0, 7) == 'Digest ') { $digest = substr($digest, 7); } $keys = array(); $match = array(); $req = array('nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1); preg_match_all('@(\w+)=([\'"]?)([a-zA-Z0-9=./\_-]+)\2@', $digest, $match, PREG_SET_ORDER); foreach ($match as $i) { $keys[$i[1]] = $i[3]; unset($req[$i[1]]); } if (empty($req)) { return $keys; } return null; } /** * Generates a hash to be compared with an HTTP digest-authenticated response * * @param array $data HTTP digest response data, as parsed by SecurityComponent::parseDigestAuthData() * @return string Digest authentication hash * @access public * @see SecurityComponent::parseDigestAuthData() */ function generateDigestResponseHash($data) { return md5( md5($data['username'] . ':' . $this->loginOptions['realm'] . ':' . $this->loginUsers[$data['username']]) . ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . md5(env('REQUEST_METHOD') . ':' . $data['uri']) ); } /** * Black-hole an invalid request with a 404 error or custom callback. If SecurityComponent::$blackHoleCallback * is specified, it will use this callback by executing the method indicated in $error * * @param object $controller Instantiating controller * @param string $error Error method * @return mixed If specified, controller blackHoleCallback's response, or no return otherwise * @access public * @see SecurityComponent::$blackHoleCallback */ function blackHole(&$controller, $error = '') { $this->Session->delete('_Token'); if ($this->blackHoleCallback == null) { $code = 404; if ($error == 'login') { $code = 401; $controller->header($this->loginRequest()); } $controller->redirect(null, $code, true); } else { return $this->_callback($controller, $this->blackHoleCallback, array($error)); } } /** * Sets the actions that require a $method HTTP request, or empty for all actions * * @param string $method The HTTP method to assign controller actions to * @param array $actions Controller actions to set the required HTTP method to. * @return void * @access protected */ function _requireMethod($method, $actions = array()) { $this->{'require' . $method} = (empty($actions)) ? array('*'): $actions; } /** * Check if HTTP methods are required * * @param object $controller Instantiating controller * @return bool true if $method is required * @access protected */ function _methodsRequired(&$controller) { foreach (array('Post', 'Get', 'Put', 'Delete') as $method) { $property = 'require' . $method; if (is_array($this->$property) && !empty($this->$property)) { $require = array_map('strtolower', $this->$property); if (in_array($this->_action, $require) || $this->$property == array('*')) { if (!$this->RequestHandler->{'is' . $method}()) { if (!$this->blackHole($controller, strtolower($method))) { return null; } } } } } return true; } /** * Check if access requires secure connection * * @param object $controller Instantiating controller * @return bool true if secure connection required * @access protected */ function _secureRequired(&$controller) { if (is_array($this->requireSecure) && !empty($this->requireSecure)) { $requireSecure = array_map('strtolower', $this->requireSecure); if (in_array($this->_action, $requireSecure) || $this->requireSecure == array('*')) { if (!$this->RequestHandler->isSSL()) { if (!$this->blackHole($controller, 'secure')) { return null; } } } } return true; } /** * Check if authentication is required * * @param object $controller Instantiating controller * @return bool true if authentication required * @access protected */ function _authRequired(&$controller) { if (is_array($this->requireAuth) && !empty($this->requireAuth) && !empty($controller->data)) { $requireAuth = array_map('strtolower', $this->requireAuth); if (in_array($this->_action, $requireAuth) || $this->requireAuth == array('*')) { if (!isset($controller->data['_Token'] )) { if (!$this->blackHole($controller, 'auth')) { return null; } } if ($this->Session->check('_Token')) { $tData = unserialize($this->Session->read('_Token')); if (!empty($tData['allowedControllers']) && !in_array($controller->params['controller'], $tData['allowedControllers']) || !empty($tData['allowedActions']) && !in_array($controller->params['action'], $tData['allowedActions'])) { if (!$this->blackHole($controller, 'auth')) { return null; } } } else { if (!$this->blackHole($controller, 'auth')) { return null; } } } } return true; } /** * Check if login is required * * @param object $controller Instantiating controller * @return bool true if login is required * @access protected */ function _loginRequired(&$controller) { if (is_array($this->requireLogin) && !empty($this->requireLogin)) { $requireLogin = array_map('strtolower', $this->requireLogin); if (in_array($this->_action, $requireLogin) || $this->requireLogin == array('*')) { $login = $this->loginCredentials($this->loginOptions['type']); if ($login == null) { $controller->header($this->loginRequest()); if (!empty($this->loginOptions['prompt'])) { $this->_callback($controller, $this->loginOptions['prompt']); } else { $this->blackHole($controller, 'login'); } } else { if (isset($this->loginOptions['login'])) { $this->_callback($controller, $this->loginOptions['login'], array($login)); } else { if (strtolower($this->loginOptions['type']) == 'digest') { if ($login && isset($this->loginUsers[$login['username']])) { if ($login['response'] == $this->generateDigestResponseHash($login)) { return true; } } $this->blackHole($controller, 'login'); } else { if ( !(in_array($login['username'], array_keys($this->loginUsers)) && $this->loginUsers[$login['username']] == $login['password']) ) { $this->blackHole($controller, 'login'); } } } } } } return true; } /** * Validate submitted form * * @param object $controller Instantiating controller * @return bool true if submitted form is valid * @access protected */ function _validatePost(&$controller) { if (empty($controller->data)) { return true; } $data = $controller->data; if (!isset($data['_Token']) || !isset($data['_Token']['fields'])) { return false; } $token = $data['_Token']['key']; if ($this->Session->check('_Token')) { $tokenData = unserialize($this->Session->read('_Token')); if ($tokenData['expires'] < time() || $tokenData['key'] !== $token) { return false; } } $locked = null; $check = $controller->data; $token = urldecode($check['_Token']['fields']); if (strpos($token, ':')) { list($token, $locked) = explode(':', $token, 2); } unset($check['_Token']); $lockedFields = array(); $fields = Set::flatten($check); $fieldList = array_keys($fields); $locked = unserialize(str_rot13($locked)); $multi = array(); foreach ($fieldList as $i => $key) { if (preg_match('/\.\d+$/', $key)) { $multi[$i] = preg_replace('/\.\d+$/', '', $key); unset($fieldList[$i]); } } if (!empty($multi)) { $fieldList += array_unique($multi); } foreach ($fieldList as $i => $key) { $isDisabled = false; $isLocked = (is_array($locked) && in_array($key, $locked)); if (!empty($this->disabledFields)) { foreach ((array)$this->disabledFields as $disabled) { $disabled = explode('.', $disabled); $field = array_values(array_intersect(explode('.', $key), $disabled)); $isDisabled = ($field === $disabled); if ($isDisabled) { break; } } } if ($isDisabled || $isLocked) { unset($fieldList[$i]); if ($isLocked) { $lockedFields[$key] = $fields[$key]; } } } sort($fieldList, SORT_STRING); ksort($lockedFields, SORT_STRING); $fieldList += $lockedFields; $check = Security::hash(serialize($fieldList) . Configure::read('Security.salt')); return ($token === $check); } /** * Add authentication key for new form posts * * @param object $controller Instantiating controller * @return bool Success * @access protected */ function _generateToken(&$controller) { if (isset($controller->params['requested']) && $controller->params['requested'] === 1) { return false; } $authKey = Security::generateAuthKey(); $expires = strtotime('+' . Security::inactiveMins() . ' minutes'); $token = array( 'key' => $authKey, 'expires' => $expires, 'allowedControllers' => $this->allowedControllers, 'allowedActions' => $this->allowedActions, 'disabledFields' => $this->disabledFields ); if (!isset($controller->data)) { $controller->data = array(); } if ($this->Session->check('_Token')) { $tokenData = unserialize($this->Session->read('_Token')); $valid = ( isset($tokenData['expires']) && $tokenData['expires'] > time() && isset($tokenData['key']) ); if ($valid) { $token['key'] = $tokenData['key']; } } $controller->params['_Token'] = $token; $this->Session->write('_Token', serialize($token)); return true; } /** * Sets the default login options for an HTTP-authenticated request * * @param array $options Default login options * @return void * @access protected */ function _setLoginDefaults(&$options) { $options = array_merge(array( 'type' => 'basic', 'realm' => env('SERVER_NAME'), 'qop' => 'auth', 'nonce' => String::uuid() ), array_filter($options)); $options = array_merge(array('opaque' => md5($options['realm'])), $options); } /** * Calls a controller callback method * * @param object $controller Controller to run callback on * @param string $method Method to execute * @param array $params Parameters to send to method * @return mixed Controller callback method's response * @access protected */ function _callback(&$controller, $method, $params = array()) { if (is_callable(array($controller, $method))) { return call_user_func_array(array(&$controller, $method), empty($params) ? null : $params); } else { // Debug::warning('Callback method ' . $method . ' in controller ' . get_class($controller) return null; } } } ?>