1<?php 2/** 3 * Security Component 4 * 5 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org) 6 * Copyright (c) Cake Software Foundation, Inc. (https://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. (https://cakefoundation.org) 13 * @link https://cakephp.org CakePHP(tm) Project 14 * @package Cake.Controller.Component 15 * @since CakePHP(tm) v 0.10.8.2156 16 * @license https://opensource.org/licenses/mit-license.php MIT License 17 */ 18 19App::uses('Component', 'Controller'); 20App::uses('CakeText', 'Utility'); 21App::uses('Hash', 'Utility'); 22App::uses('Security', 'Utility'); 23 24/** 25 * The Security Component creates an easy way to integrate tighter security in 26 * your application. It provides methods for various tasks like: 27 * 28 * - Restricting which HTTP methods your application accepts. 29 * - CSRF protection. 30 * - Form tampering protection 31 * - Requiring that SSL be used. 32 * - Limiting cross controller communication. 33 * 34 * @package Cake.Controller.Component 35 * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html 36 */ 37class SecurityComponent extends Component { 38 39/** 40 * Default message used for exceptions thrown 41 */ 42 const DEFAULT_EXCEPTION_MESSAGE = 'The request has been black-holed'; 43 44/** 45 * The controller method that will be called if this request is black-hole'd 46 * 47 * @var string 48 */ 49 public $blackHoleCallback = null; 50 51/** 52 * List of controller actions for which a POST request is required 53 * 54 * @var array 55 * @deprecated 3.0.0 Use CakeRequest::allowMethod() instead. 56 * @see SecurityComponent::requirePost() 57 */ 58 public $requirePost = array(); 59 60/** 61 * List of controller actions for which a GET request is required 62 * 63 * @var array 64 * @deprecated 3.0.0 Use CakeRequest::allowMethod() instead. 65 * @see SecurityComponent::requireGet() 66 */ 67 public $requireGet = array(); 68 69/** 70 * List of controller actions for which a PUT request is required 71 * 72 * @var array 73 * @deprecated 3.0.0 Use CakeRequest::allowMethod() instead. 74 * @see SecurityComponent::requirePut() 75 */ 76 public $requirePut = array(); 77 78/** 79 * List of controller actions for which a DELETE request is required 80 * 81 * @var array 82 * @deprecated 3.0.0 Use CakeRequest::allowMethod() instead. 83 * @see SecurityComponent::requireDelete() 84 */ 85 public $requireDelete = array(); 86 87/** 88 * List of actions that require an SSL-secured connection 89 * 90 * @var array 91 * @see SecurityComponent::requireSecure() 92 */ 93 public $requireSecure = array(); 94 95/** 96 * List of actions that require a valid authentication key 97 * 98 * @var array 99 * @see SecurityComponent::requireAuth() 100 * @deprecated 2.8.1 This feature is confusing and not useful. 101 */ 102 public $requireAuth = array(); 103 104/** 105 * Controllers from which actions of the current controller are allowed to receive 106 * requests. 107 * 108 * @var array 109 * @see SecurityComponent::requireAuth() 110 */ 111 public $allowedControllers = array(); 112 113/** 114 * Actions from which actions of the current controller are allowed to receive 115 * requests. 116 * 117 * @var array 118 * @see SecurityComponent::requireAuth() 119 */ 120 public $allowedActions = array(); 121 122/** 123 * Deprecated property, superseded by unlockedFields. 124 * 125 * @var array 126 * @deprecated 3.0.0 Superseded by unlockedFields. 127 * @see SecurityComponent::$unlockedFields 128 */ 129 public $disabledFields = array(); 130 131/** 132 * Form fields to exclude from POST validation. Fields can be unlocked 133 * either in the Component, or with FormHelper::unlockField(). 134 * Fields that have been unlocked are not required to be part of the POST 135 * and hidden unlocked fields do not have their values checked. 136 * 137 * @var array 138 */ 139 public $unlockedFields = array(); 140 141/** 142 * Actions to exclude from CSRF and POST validation checks. 143 * Other checks like requireAuth(), requireSecure(), 144 * requirePost(), requireGet() etc. will still be applied. 145 * 146 * @var array 147 */ 148 public $unlockedActions = array(); 149 150/** 151 * Whether to validate POST data. Set to false to disable for data coming from 3rd party 152 * services, etc. 153 * 154 * @var bool 155 */ 156 public $validatePost = true; 157 158/** 159 * Whether to use CSRF protected forms. Set to false to disable CSRF protection on forms. 160 * 161 * @var bool 162 * @see http://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF) 163 * @see SecurityComponent::$csrfExpires 164 */ 165 public $csrfCheck = true; 166 167/** 168 * The duration from when a CSRF token is created that it will expire on. 169 * Each form/page request will generate a new token that can only be submitted once unless 170 * it expires. Can be any value compatible with strtotime() 171 * 172 * @var string 173 */ 174 public $csrfExpires = '+30 minutes'; 175 176/** 177 * Controls whether or not CSRF tokens are use and burn. Set to false to not generate 178 * new tokens on each request. One token will be reused until it expires. This reduces 179 * the chances of users getting invalid requests because of token consumption. 180 * It has the side effect of making CSRF less secure, as tokens are reusable. 181 * 182 * @var bool 183 */ 184 public $csrfUseOnce = true; 185 186/** 187 * Control the number of tokens a user can keep open. 188 * This is most useful with one-time use tokens. Since new tokens 189 * are created on each request, having a hard limit on the number of open tokens 190 * can be useful in controlling the size of the session file. 191 * 192 * When tokens are evicted, the oldest ones will be removed, as they are the most likely 193 * to be dead/expired. 194 * 195 * @var int 196 */ 197 public $csrfLimit = 100; 198 199/** 200 * Other components used by the Security component 201 * 202 * @var array 203 */ 204 public $components = array('Session'); 205 206/** 207 * Holds the current action of the controller 208 * 209 * @var string 210 */ 211 protected $_action = null; 212 213/** 214 * Request object 215 * 216 * @var CakeRequest 217 */ 218 public $request; 219 220/** 221 * Component startup. All security checking happens here. 222 * 223 * @param Controller $controller Instantiating controller 224 * @throws AuthSecurityException 225 * @return void 226 */ 227 public function startup(Controller $controller) { 228 $this->request = $controller->request; 229 $this->_action = $controller->request->params['action']; 230 $hasData = ($controller->request->data || $controller->request->is(array('put', 'post', 'delete', 'patch'))); 231 try { 232 $this->_methodsRequired($controller); 233 $this->_secureRequired($controller); 234 $this->_authRequired($controller); 235 236 $isNotRequestAction = ( 237 !isset($controller->request->params['requested']) || 238 $controller->request->params['requested'] != 1 239 ); 240 241 if ($this->_action === $this->blackHoleCallback) { 242 throw new AuthSecurityException(sprintf('Action %s is defined as the blackhole callback.', $this->_action)); 243 } 244 245 if (!in_array($this->_action, (array)$this->unlockedActions) && $hasData && $isNotRequestAction) { 246 if ($this->validatePost) { 247 $this->_validatePost($controller); 248 } 249 if ($this->csrfCheck) { 250 $this->_validateCsrf($controller); 251 } 252 } 253 254 } catch (SecurityException $se) { 255 return $this->blackHole($controller, $se->getType(), $se); 256 } 257 258 $this->generateToken($controller->request); 259 if ($hasData && is_array($controller->request->data)) { 260 unset($controller->request->data['_Token']); 261 } 262 } 263 264/** 265 * Sets the actions that require a POST request, or empty for all actions 266 * 267 * @return void 268 * @deprecated 3.0.0 Use CakeRequest::onlyAllow() instead. 269 * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#SecurityComponent::requirePost 270 */ 271 public function requirePost() { 272 $args = func_get_args(); 273 $this->_requireMethod('Post', $args); 274 } 275 276/** 277 * Sets the actions that require a GET request, or empty for all actions 278 * 279 * @deprecated 3.0.0 Use CakeRequest::onlyAllow() instead. 280 * @return void 281 */ 282 public function requireGet() { 283 $args = func_get_args(); 284 $this->_requireMethod('Get', $args); 285 } 286 287/** 288 * Sets the actions that require a PUT request, or empty for all actions 289 * 290 * @deprecated 3.0.0 Use CakeRequest::onlyAllow() instead. 291 * @return void 292 */ 293 public function requirePut() { 294 $args = func_get_args(); 295 $this->_requireMethod('Put', $args); 296 } 297 298/** 299 * Sets the actions that require a DELETE request, or empty for all actions 300 * 301 * @deprecated 3.0.0 Use CakeRequest::onlyAllow() instead. 302 * @return void 303 */ 304 public function requireDelete() { 305 $args = func_get_args(); 306 $this->_requireMethod('Delete', $args); 307 } 308 309/** 310 * Sets the actions that require a request that is SSL-secured, or empty for all actions 311 * 312 * @return void 313 * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#SecurityComponent::requireSecure 314 */ 315 public function requireSecure() { 316 $args = func_get_args(); 317 $this->_requireMethod('Secure', $args); 318 } 319 320/** 321 * Sets the actions that require whitelisted form submissions. 322 * 323 * Adding actions with this method will enforce the restrictions 324 * set in SecurityComponent::$allowedControllers and 325 * SecurityComponent::$allowedActions. 326 * 327 * @return void 328 * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#SecurityComponent::requireAuth 329 */ 330 public function requireAuth() { 331 $args = func_get_args(); 332 $this->_requireMethod('Auth', $args); 333 } 334 335/** 336 * Black-hole an invalid request with a 400 error or custom callback. If SecurityComponent::$blackHoleCallback 337 * is specified, it will use this callback by executing the method indicated in $error 338 * 339 * @param Controller $controller Instantiating controller 340 * @param string $error Error method 341 * @param SecurityException|null $exception Additional debug info describing the cause 342 * @return mixed If specified, controller blackHoleCallback's response, or no return otherwise 343 * @see SecurityComponent::$blackHoleCallback 344 * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#handling-blackhole-callbacks 345 * @throws BadRequestException 346 */ 347 public function blackHole(Controller $controller, $error = '', SecurityException $exception = null) { 348 if (!$this->blackHoleCallback) { 349 $this->_throwException($exception); 350 } 351 return $this->_callback($controller, $this->blackHoleCallback, array($error)); 352 } 353 354/** 355 * Check debug status and throw an Exception based on the existing one 356 * 357 * @param SecurityException|null $exception Additional debug info describing the cause 358 * @throws BadRequestException 359 * @return void 360 */ 361 protected function _throwException($exception = null) { 362 if ($exception !== null) { 363 if (!Configure::read('debug') && $exception instanceof SecurityException) { 364 $exception->setReason($exception->getMessage()); 365 $exception->setMessage(self::DEFAULT_EXCEPTION_MESSAGE); 366 } 367 throw $exception; 368 } 369 throw new BadRequestException(self::DEFAULT_EXCEPTION_MESSAGE); 370 } 371 372/** 373 * Sets the actions that require a $method HTTP request, or empty for all actions 374 * 375 * @param string $method The HTTP method to assign controller actions to 376 * @param array $actions Controller actions to set the required HTTP method to. 377 * @return void 378 */ 379 protected function _requireMethod($method, $actions = array()) { 380 if (isset($actions[0]) && is_array($actions[0])) { 381 $actions = $actions[0]; 382 } 383 $this->{'require' . $method} = (empty($actions)) ? array('*') : $actions; 384 } 385 386/** 387 * Check if HTTP methods are required 388 * 389 * @param Controller $controller Instantiating controller 390 * @throws SecurityException 391 * @return bool True if $method is required 392 */ 393 protected function _methodsRequired(Controller $controller) { 394 foreach (array('Post', 'Get', 'Put', 'Delete') as $method) { 395 $property = 'require' . $method; 396 if (is_array($this->$property) && !empty($this->$property)) { 397 $require = $this->$property; 398 if (in_array($this->_action, $require) || $this->$property === array('*')) { 399 if (!$controller->request->is($method)) { 400 throw new SecurityException( 401 sprintf('The request method must be %s', strtoupper($method)) 402 ); 403 } 404 } 405 } 406 } 407 return true; 408 } 409 410/** 411 * Check if access requires secure connection 412 * 413 * @param Controller $controller Instantiating controller 414 * @throws SecurityException 415 * @return bool True if secure connection required 416 */ 417 protected function _secureRequired(Controller $controller) { 418 if (is_array($this->requireSecure) && !empty($this->requireSecure)) { 419 $requireSecure = $this->requireSecure; 420 421 if (in_array($this->_action, $requireSecure) || $this->requireSecure === array('*')) { 422 if (!$controller->request->is('ssl')) { 423 throw new SecurityException( 424 'Request is not SSL and the action is required to be secure' 425 ); 426 } 427 } 428 } 429 return true; 430 } 431 432/** 433 * Check if authentication is required 434 * 435 * @param Controller $controller Instantiating controller 436 * @return bool|null True if authentication required 437 * @throws AuthSecurityException 438 * @deprecated 2.8.1 This feature is confusing and not useful. 439 */ 440 protected function _authRequired(Controller $controller) { 441 if (is_array($this->requireAuth) && !empty($this->requireAuth) && !empty($controller->request->data)) { 442 $requireAuth = $this->requireAuth; 443 444 if (in_array($controller->request->params['action'], $requireAuth) || $this->requireAuth === array('*')) { 445 if (!isset($controller->request->data['_Token'])) { 446 throw new AuthSecurityException('\'_Token\' was not found in request data.'); 447 } 448 449 if ($this->Session->check('_Token')) { 450 $tData = $this->Session->read('_Token'); 451 452 if (!empty($tData['allowedControllers']) && 453 !in_array($controller->request->params['controller'], $tData['allowedControllers'])) { 454 throw new AuthSecurityException( 455 sprintf( 456 'Controller \'%s\' was not found in allowed controllers: \'%s\'.', 457 $controller->request->params['controller'], 458 implode(', ', (array)$tData['allowedControllers']) 459 ) 460 ); 461 } 462 if (!empty($tData['allowedActions']) && 463 !in_array($controller->request->params['action'], $tData['allowedActions']) 464 ) { 465 throw new AuthSecurityException( 466 sprintf( 467 'Action \'%s::%s\' was not found in allowed actions: \'%s\'.', 468 $controller->request->params['controller'], 469 $controller->request->params['action'], 470 implode(', ', (array)$tData['allowedActions']) 471 ) 472 ); 473 } 474 } else { 475 throw new AuthSecurityException('\'_Token\' was not found in session.'); 476 } 477 } 478 } 479 return true; 480 } 481 482/** 483 * Validate submitted form 484 * 485 * @param Controller $controller Instantiating controller 486 * @throws AuthSecurityException 487 * @return bool true if submitted form is valid 488 */ 489 protected function _validatePost(Controller $controller) { 490 $token = $this->_validToken($controller); 491 $hashParts = $this->_hashParts($controller); 492 $check = Security::hash(implode('', $hashParts), 'sha1'); 493 494 if ($token === $check) { 495 return true; 496 } 497 498 $msg = self::DEFAULT_EXCEPTION_MESSAGE; 499 if (Configure::read('debug')) { 500 $msg = $this->_debugPostTokenNotMatching($controller, $hashParts); 501 } 502 503 throw new AuthSecurityException($msg); 504 } 505 506/** 507 * Check if token is valid 508 * 509 * @param Controller $controller Instantiating controller 510 * @throws AuthSecurityException 511 * @throws SecurityException 512 * @return string fields token 513 */ 514 protected function _validToken(Controller $controller) { 515 $check = $controller->request->data; 516 517 $message = '\'%s\' was not found in request data.'; 518 if (!isset($check['_Token'])) { 519 throw new AuthSecurityException(sprintf($message, '_Token')); 520 } 521 if (!isset($check['_Token']['fields'])) { 522 throw new AuthSecurityException(sprintf($message, '_Token.fields')); 523 } 524 if (!isset($check['_Token']['unlocked'])) { 525 throw new AuthSecurityException(sprintf($message, '_Token.unlocked')); 526 } 527 if (Configure::read('debug') && !isset($check['_Token']['debug'])) { 528 throw new SecurityException(sprintf($message, '_Token.debug')); 529 } 530 if (!Configure::read('debug') && isset($check['_Token']['debug'])) { 531 throw new SecurityException('Unexpected \'_Token.debug\' found in request data'); 532 } 533 534 $token = urldecode($check['_Token']['fields']); 535 if (strpos($token, ':')) { 536 list($token, ) = explode(':', $token, 2); 537 } 538 539 return $token; 540 } 541 542/** 543 * Return hash parts for the Token generation 544 * 545 * @param Controller $controller Instantiating controller 546 * @return array 547 */ 548 protected function _hashParts(Controller $controller) { 549 $fieldList = $this->_fieldsList($controller->request->data); 550 $unlocked = $this->_sortedUnlocked($controller->request->data); 551 552 return array( 553 $controller->request->here(), 554 serialize($fieldList), 555 $unlocked, 556 Configure::read('Security.salt') 557 ); 558 } 559 560/** 561 * Return the fields list for the hash calculation 562 * 563 * @param array $check Data array 564 * @return array 565 */ 566 protected function _fieldsList(array $check) { 567 $locked = ''; 568 $token = urldecode($check['_Token']['fields']); 569 $unlocked = $this->_unlocked($check); 570 571 if (strpos($token, ':')) { 572 list($token, $locked) = explode(':', $token, 2); 573 } 574 unset($check['_Token'], $check['_csrfToken']); 575 576 $locked = explode('|', $locked); 577 $unlocked = explode('|', $unlocked); 578 579 $fields = Hash::flatten($check); 580 $fieldList = array_keys($fields); 581 $multi = $lockedFields = array(); 582 $isUnlocked = false; 583 584 foreach ($fieldList as $i => $key) { 585 if (preg_match('/(\.\d+){1,10}$/', $key)) { 586 $multi[$i] = preg_replace('/(\.\d+){1,10}$/', '', $key); 587 unset($fieldList[$i]); 588 } else { 589 $fieldList[$i] = (string)$key; 590 } 591 } 592 if (!empty($multi)) { 593 $fieldList += array_unique($multi); 594 } 595 596 $unlockedFields = array_unique( 597 array_merge((array)$this->disabledFields, (array)$this->unlockedFields, $unlocked) 598 ); 599 600 foreach ($fieldList as $i => $key) { 601 $isLocked = (is_array($locked) && in_array($key, $locked)); 602 603 if (!empty($unlockedFields)) { 604 foreach ($unlockedFields as $off) { 605 $off = explode('.', $off); 606 $field = array_values(array_intersect(explode('.', $key), $off)); 607 $isUnlocked = ($field === $off); 608 if ($isUnlocked) { 609 break; 610 } 611 } 612 } 613 614 if ($isUnlocked || $isLocked) { 615 unset($fieldList[$i]); 616 if ($isLocked) { 617 $lockedFields[$key] = $fields[$key]; 618 } 619 } 620 } 621 sort($fieldList, SORT_STRING); 622 ksort($lockedFields, SORT_STRING); 623 $fieldList += $lockedFields; 624 625 return $fieldList; 626 } 627 628/** 629 * Get the unlocked string 630 * 631 * @param array $data Data array 632 * @return string 633 */ 634 protected function _unlocked(array $data) { 635 return urldecode($data['_Token']['unlocked']); 636 } 637 638/** 639 * Get the sorted unlocked string 640 * 641 * @param array $data Data array 642 * @return string 643 */ 644 protected function _sortedUnlocked($data) { 645 $unlocked = $this->_unlocked($data); 646 $unlocked = explode('|', $unlocked); 647 sort($unlocked, SORT_STRING); 648 649 return implode('|', $unlocked); 650 } 651 652/** 653 * Create a message for humans to understand why Security token is not matching 654 * 655 * @param Controller $controller Instantiating controller 656 * @param array $hashParts Elements used to generate the Token hash 657 * @return string Message explaining why the tokens are not matching 658 */ 659 protected function _debugPostTokenNotMatching(Controller $controller, $hashParts) { 660 $messages = array(); 661 $expectedParts = json_decode(urldecode($controller->request->data['_Token']['debug']), true); 662 if (!is_array($expectedParts) || count($expectedParts) !== 3) { 663 return 'Invalid security debug token.'; 664 } 665 $expectedUrl = Hash::get($expectedParts, 0); 666 $url = Hash::get($hashParts, 0); 667 if ($expectedUrl !== $url) { 668 $messages[] = sprintf('URL mismatch in POST data (expected \'%s\' but found \'%s\')', $expectedUrl, $url); 669 } 670 $expectedFields = Hash::get($expectedParts, 1); 671 $dataFields = Hash::get($hashParts, 1); 672 if ($dataFields) { 673 $dataFields = unserialize($dataFields); 674 } 675 $fieldsMessages = $this->_debugCheckFields( 676 $dataFields, 677 $expectedFields, 678 'Unexpected field \'%s\' in POST data', 679 'Tampered field \'%s\' in POST data (expected value \'%s\' but found \'%s\')', 680 'Missing field \'%s\' in POST data' 681 ); 682 $expectedUnlockedFields = Hash::get($expectedParts, 2); 683 $dataUnlockedFields = Hash::get($hashParts, 2) ?: array(); 684 if ($dataUnlockedFields) { 685 $dataUnlockedFields = explode('|', $dataUnlockedFields); 686 } 687 $unlockFieldsMessages = $this->_debugCheckFields( 688 $dataUnlockedFields, 689 $expectedUnlockedFields, 690 'Unexpected unlocked field \'%s\' in POST data', 691 null, 692 'Missing unlocked field: \'%s\'' 693 ); 694 695 $messages = array_merge($messages, $fieldsMessages, $unlockFieldsMessages); 696 697 return implode(', ', $messages); 698 } 699 700/** 701 * Iterates data array to check against expected 702 * 703 * @param array $dataFields Fields array, containing the POST data fields 704 * @param array $expectedFields Fields array, containing the expected fields we should have in POST 705 * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected) 706 * @param string $stringKeyMessage Message string if tampered found in data fields indexed by string (protected) 707 * @param string $missingMessage Message string if missing field 708 * @return array Messages 709 */ 710 protected function _debugCheckFields($dataFields, $expectedFields = array(), $intKeyMessage = '', $stringKeyMessage = '', $missingMessage = '') { 711 $messages = $this->_matchExistingFields($dataFields, $expectedFields, $intKeyMessage, $stringKeyMessage); 712 $expectedFieldsMessage = $this->_debugExpectedFields($expectedFields, $missingMessage); 713 if ($expectedFieldsMessage !== null) { 714 $messages[] = $expectedFieldsMessage; 715 } 716 717 return $messages; 718 } 719 720/** 721 * Manually add CSRF token information into the provided request object. 722 * 723 * @param CakeRequest $request The request object to add into. 724 * @return bool 725 */ 726 public function generateToken(CakeRequest $request) { 727 if (isset($request->params['requested']) && $request->params['requested'] === 1) { 728 if ($this->Session->check('_Token')) { 729 $request->params['_Token'] = $this->Session->read('_Token'); 730 } 731 return false; 732 } 733 $authKey = hash('sha512', Security::randomBytes(16), false); 734 $token = array( 735 'key' => $authKey, 736 'allowedControllers' => $this->allowedControllers, 737 'allowedActions' => $this->allowedActions, 738 'unlockedFields' => array_merge($this->disabledFields, $this->unlockedFields), 739 'csrfTokens' => array() 740 ); 741 742 $tokenData = array(); 743 if ($this->Session->check('_Token')) { 744 $tokenData = $this->Session->read('_Token'); 745 if (!empty($tokenData['csrfTokens']) && is_array($tokenData['csrfTokens'])) { 746 $token['csrfTokens'] = $this->_expireTokens($tokenData['csrfTokens']); 747 } 748 } 749 if ($this->csrfUseOnce || empty($token['csrfTokens'])) { 750 $token['csrfTokens'][$authKey] = strtotime($this->csrfExpires); 751 } 752 if (!$this->csrfUseOnce) { 753 $csrfTokens = array_keys($token['csrfTokens']); 754 $authKey = $csrfTokens[0]; 755 $token['key'] = $authKey; 756 $token['csrfTokens'][$authKey] = strtotime($this->csrfExpires); 757 } 758 $this->Session->write('_Token', $token); 759 $request->params['_Token'] = array( 760 'key' => $token['key'], 761 'unlockedFields' => $token['unlockedFields'] 762 ); 763 return true; 764 } 765 766/** 767 * Validate that the controller has a CSRF token in the POST data 768 * and that the token is legit/not expired. If the token is valid 769 * it will be removed from the list of valid tokens. 770 * 771 * @param Controller $controller A controller to check 772 * @throws SecurityException 773 * @return bool Valid csrf token. 774 */ 775 protected function _validateCsrf(Controller $controller) { 776 $token = $this->Session->read('_Token'); 777 $requestToken = $controller->request->data('_Token.key'); 778 779 if (!$requestToken) { 780 throw new SecurityException('Missing CSRF token'); 781 } 782 783 if (!isset($token['csrfTokens'][$requestToken])) { 784 throw new SecurityException('CSRF token mismatch'); 785 } 786 787 if ($token['csrfTokens'][$requestToken] < time()) { 788 throw new SecurityException('CSRF token expired'); 789 } 790 791 if ($this->csrfUseOnce) { 792 $this->Session->delete('_Token.csrfTokens.' . $requestToken); 793 } 794 return true; 795 } 796 797/** 798 * Expire CSRF nonces and remove them from the valid tokens. 799 * Uses a simple timeout to expire the tokens. 800 * 801 * @param array $tokens An array of nonce => expires. 802 * @return array An array of nonce => expires. 803 */ 804 protected function _expireTokens($tokens) { 805 $now = time(); 806 foreach ($tokens as $nonce => $expires) { 807 if ($expires < $now) { 808 unset($tokens[$nonce]); 809 } 810 } 811 $overflow = count($tokens) - $this->csrfLimit; 812 if ($overflow > 0) { 813 $tokens = array_slice($tokens, $overflow + 1, null, true); 814 } 815 return $tokens; 816 } 817 818/** 819 * Calls a controller callback method 820 * 821 * @param Controller $controller Controller to run callback on 822 * @param string $method Method to execute 823 * @param array $params Parameters to send to method 824 * @return mixed Controller callback method's response 825 * @throws BadRequestException When a the blackholeCallback is not callable. 826 */ 827 protected function _callback(Controller $controller, $method, $params = array()) { 828 if (!is_callable(array($controller, $method))) { 829 throw new BadRequestException(__d('cake_dev', 'The request has been black-holed')); 830 } 831 return call_user_func_array(array(&$controller, $method), empty($params) ? null : $params); 832 } 833 834/** 835 * Generate array of messages for the existing fields in POST data, matching dataFields in $expectedFields 836 * will be unset 837 * 838 * @param array $dataFields Fields array, containing the POST data fields 839 * @param array &$expectedFields Fields array, containing the expected fields we should have in POST 840 * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected) 841 * @param string $stringKeyMessage Message string if tampered found in data fields indexed by string (protected) 842 * @return array Error messages 843 */ 844 protected function _matchExistingFields($dataFields, &$expectedFields, $intKeyMessage, $stringKeyMessage) { 845 $messages = array(); 846 foreach ((array)$dataFields as $key => $value) { 847 if (is_int($key)) { 848 $foundKey = array_search($value, (array)$expectedFields); 849 if ($foundKey === false) { 850 $messages[] = sprintf($intKeyMessage, $value); 851 } else { 852 unset($expectedFields[$foundKey]); 853 } 854 } elseif (is_string($key)) { 855 if (isset($expectedFields[$key]) && $value !== $expectedFields[$key]) { 856 $messages[] = sprintf($stringKeyMessage, $key, $expectedFields[$key], $value); 857 } 858 unset($expectedFields[$key]); 859 } 860 } 861 862 return $messages; 863 } 864 865/** 866 * Generate debug message for the expected fields 867 * 868 * @param array $expectedFields Expected fields 869 * @param string $missingMessage Message template 870 * @return string Error message about expected fields 871 */ 872 protected function _debugExpectedFields($expectedFields = array(), $missingMessage = '') { 873 if (count($expectedFields) === 0) { 874 return null; 875 } 876 877 $expectedFieldNames = array(); 878 foreach ((array)$expectedFields as $key => $expectedField) { 879 if (is_int($key)) { 880 $expectedFieldNames[] = $expectedField; 881 } else { 882 $expectedFieldNames[] = $key; 883 } 884 } 885 886 return sprintf($missingMessage, implode(', ', $expectedFieldNames)); 887 } 888 889} 890