diff --git a/cake/libs/controller/components/security.php b/cake/libs/controller/components/security.php index b1b14bc8f..c300da19f 100644 --- a/cake/libs/controller/components/security.php +++ b/cake/libs/controller/components/security.php @@ -216,9 +216,12 @@ class SecurityComponent extends Component { if ($isPost && $isRequestAction && $this->validatePost) { if ($this->_validatePost($controller) === false) { - if (!$this->blackHole($controller, 'auth')) { - return null; - } + return $this->blackHole($controller, 'auth'); + } + } + if ($isPost && $this->csrfCheck) { + if ($this->_validateCsrf($controller) === false) { + return $this->blackHole($controller, 'csrf'); } } $this->_generateToken($controller); @@ -434,7 +437,7 @@ class SecurityComponent extends Component { $code = 401; $controller->header($this->loginRequest()); } - $controller->redirect(null, $code, true); + return $controller->redirect(null, $code, true); } else { return $this->_callback($controller, $this->blackHoleCallback, array($error)); } @@ -709,6 +712,23 @@ class SecurityComponent extends Component { return true; } +/** + * Validate that the controller has a CSRF token in the POST data + * and that the token is legit/not expired. + * + * @param Controller $controller A controller to check + * @return boolean Valid csrf token. + */ + protected function _validateCsrf($controller) { + $token = $this->Session->read('_Token'); + $requestToken = $controller->request->data('_Token.nonce'); + if (isset($token['csrfTokens'][$requestToken])) { + $this->Session->delete('_Token.csrfTokens.' . $requestToken); + return true; + } + return false; + } + /** * Sets the default login options for an HTTP-authenticated request * diff --git a/cake/tests/cases/libs/controller/components/security.test.php b/cake/tests/cases/libs/controller/components/security.test.php index 1a0a0386d..30d9389ef 100644 --- a/cake/tests/cases/libs/controller/components/security.test.php +++ b/cake/tests/cases/libs/controller/components/security.test.php @@ -153,6 +153,7 @@ class SecurityComponentTest extends CakeTestCase { $this->Controller->Security = $this->Controller->TestSecurity; $this->Controller->Security->blackHoleCallback = 'fail'; $this->Security = $this->Controller->Security; + $this->Security->csrfCheck = false; Configure::write('Security.salt', 'foo!'); } @@ -233,7 +234,7 @@ class SecurityComponentTest extends CakeTestCase { $_SERVER['REQUEST_METHOD'] = 'POST'; $this->Controller->request['action'] = 'posted'; $this->Controller->Security->requirePost('posted'); - $this->Controller->Security->startup($this->Controller); + $this->Security->startup($this->Controller); $this->assertFalse($this->Controller->failed); } @@ -451,8 +452,8 @@ DIGEST; function testRequireGetSucceedWrongMethod() { $_SERVER['REQUEST_METHOD'] = 'POST'; $this->Controller->request['action'] = 'posted'; - $this->Controller->Security->requireGet('getted'); - $this->Controller->Security->startup($this->Controller); + $this->Security->requireGet('getted'); + $this->Security->startup($this->Controller); $this->assertFalse($this->Controller->failed); } @@ -1245,7 +1246,12 @@ DIGEST; $this->assertEquals(count($token['csrfTokens']), 1, 'Missing the csrf token.'); $this->assertEquals(strtotime('+10 minutes'), current($token['csrfTokens']), 'Token expiry does not match'); } - + +/** + * Test setting multiple nonces, when startup() is called more than once, (ie more than one request.) + * + * @return void + */ function testCsrfSettingMultipleNonces() { $this->Security->validatePost = false; $this->Security->csrfCheck = true; @@ -1259,4 +1265,35 @@ DIGEST; $this->assertEquals(strtotime('+10 minutes'), $expires, 'Token expiry does not match'); } } + +/** + * test that nonces are consumed by form submits. + * + * @return void + */ + function testCsrfNonceConsumption() { + $this->Security->validatePost = false; + $this->Security->csrfCheck = true; + $this->Security->csrfExpires = '+10 minutes'; + + $this->Security->Session->write('_Token.csrfTokens', array('nonce1' => strtotime('+10 minutes'))); + + $this->Controller->request = $this->getMock('CakeRequest', array('is')); + $this->Controller->request->expects($this->once())->method('is') + ->with('post') + ->will($this->returnValue(true)); + + $this->Controller->request->params['action'] = 'index'; + $this->Controller->request->data = array( + '_Token' => array( + 'nonce' => 'nonce1' + ), + 'Post' => array( + 'title' => 'Woot' + ) + ); + $this->Security->startup($this->Controller); + $token = $this->Security->Session->read('_Token'); + $this->assertFalse(isset($token['csrfTokens']['nonce1']), 'Token was not consumed'); + } }