Browse Source

fix unit tests, add wrapper to check exceptions and debug messages

Jorge González 10 years ago
parent
commit
98fd7a6f27

+ 41 - 32
src/Controller/Component/SecurityComponent.php

@@ -188,6 +188,7 @@ class SecurityComponent extends Component
      *
      * @param \Cake\Controller\Controller $controller Instantiating controller
      * @param string $error Error method
+     * @param \Cake\Controller\Exception\SecurityException $exception Additional debug info describing the cause
      * @return mixed If specified, controller blackHoleCallback's response, or no return otherwise
      * @see SecurityComponent::$blackHoleCallback
      * @link http://book.cakephp.org/3.0/en/controllers/components/security.html#handling-blackhole-callbacks
@@ -260,7 +261,7 @@ class SecurityComponent extends Component
 
             if (in_array($this->request->params['action'], $requireAuth) || $requireAuth == ['*']) {
                 if (!isset($controller->request->data['_Token'])) {
-                    throw new AuthSecurityException(sprintf('%s was not found in request data.', '_Token'));
+                    throw new AuthSecurityException(sprintf('\'%s\' was not found in request data.', '_Token'));
                 }
 
                 if ($this->session->check('_Token')) {
@@ -269,7 +270,8 @@ class SecurityComponent extends Component
                     if (!empty($tData['allowedControllers']) &&
                         !in_array($this->request->params['controller'], $tData['allowedControllers'])) {
                         throw new AuthSecurityException(
-                            sprintf('Controller %s was not found in allowed controllers: %s.',
+                            sprintf(
+                                'Controller \'%s\' was not found in allowed controllers: \'%s\'.',
                                 $this->request->params['controller'],
                                 implode(', ', (array)$tData['allowedControllers'])
                             )
@@ -279,14 +281,16 @@ class SecurityComponent extends Component
                         !in_array($this->request->params['action'], $tData['allowedActions'])
                     ) {
                         throw new AuthSecurityException(
-                            sprintf('Controller %s was not found in allowed controllers: %s.',
+                            sprintf(
+                                'Action \'%s::%s\' was not found in allowed actions: \'%s\'.',
+                                $this->request->params['controller'],
                                 $this->request->params['action'],
                                 implode(', ', (array)$tData['allowedActions'])
                             )
                         );
                     }
                 } else {
-                    throw new AuthSecurityException(sprintf('%s was not found in session.', '_Token'));
+                    throw new AuthSecurityException(sprintf('\'%s\' was not found in session.', '_Token'));
                 }
             }
         }
@@ -297,6 +301,7 @@ class SecurityComponent extends Component
      * Validate submitted form
      *
      * @param \Cake\Controller\Controller $controller Instantiating controller
+     * @throws \Cake\Controller\Exception\SecurityException
      * @return bool true if submitted form is valid
      */
     protected function _validatePost(Controller $controller)
@@ -323,15 +328,15 @@ class SecurityComponent extends Component
     /**
      * Check if token is valid
      *
-     * @param Controller $controller
-     * @throws SecurityException
-     * @return string fields token
+     * @param \Cake\Controller\Controller $controller Instantiating controller
+     * @throws \Cake\Controller\Exception\SecurityException
+     * @return String fields token
      */
     protected function _validToken(Controller $controller)
     {
         $check = $controller->request->data;
 
-        $message = '%s was not found in request data.';
+        $message = '\'%s\' was not found in request data.';
         if (!isset($check['_Token'])) {
             throw new AuthSecurityException(sprintf($message, '_Token'));
         }
@@ -355,7 +360,7 @@ class SecurityComponent extends Component
     /**
      * Return hash parts for the Token generation
      *
-     * @param Controller $controller
+     * @param \Cake\Controller\Controller $controller Instantiating controller
      * @return array
      */
     protected function _hashParts(Controller $controller)
@@ -373,7 +378,7 @@ class SecurityComponent extends Component
     /**
      * Return the fields list for the hash calculation
      *
-     * @param array $check data array
+     * @param array $check Data array
      * @return array
      */
     protected function _fieldsList(array $check)
@@ -442,7 +447,7 @@ class SecurityComponent extends Component
     /**
      * Get the unlocked string
      *
-     * @param array $check data array
+     * @param array $check Data array
      * @return string
      */
     protected function _unlocked(array $check)
@@ -453,25 +458,28 @@ class SecurityComponent extends Component
     /**
      * Create a message for humans to understand why Security token is not matching
      *
-     * @param $controller
-     * @return array messages to explain why token is not matching
+     * @param \Cake\Controller\Controller $controller Instantiating controller
+     * @param array $hashParts Elements used to generate the Token hash
+     * @return array Messages to explain why token is not matching
      */
-    protected function _debugPostTokenNotMatching($controller, $hashParts)
+    protected function _debugPostTokenNotMatching(Controller $controller, $hashParts)
     {
         $messages = [];
         $expectedParts = json_decode(urldecode($controller->request->data['_Token']['debug']), true);
-        //@todo check array and counts for expected and data parts
+        if (!is_array($expectedParts) || count($expectedParts) !== 3) {
+            return 'Invalid security debug token.';
+        }
         if ($hashParts[0] !== $expectedParts[0]) {
-            $messages[] = sprintf('Url not matching, \'%s\' was expected, but \'%s\' was sent in post data.', $expectedParts[0], $hashParts[0]);
+            $messages[] = sprintf('URL mismatch in POST data (expected \'%s\' but found \'%s\')', $expectedParts[0], $hashParts[0]);
         }
         $expectedFields = $expectedParts[1];
         $dataFields = unserialize($hashParts[1]);
         $fieldsMessages = $this->_debugCheckFields(
             $dataFields,
             $expectedFields,
-            'On request data field \'%s\', was injected and not expected',
-            'On request data field \'%s\', expected value was \'%s\', not matching with post data value \'%s\'',
-            'On request data there were missing expected fields: \'%s\''
+            'Unexpected field \'%s\' in POST data',
+            'Tampered field \'%s\' in POST data (expected value \'%s\' but found \'%s\')',
+            'Missing field \'%s\' in POST data'
         );
         $expectedUnlockedFields = Hash::get($expectedParts, 2);
         $dataUnlockedFields = Hash::get($hashParts, 2) ?: [];
@@ -481,44 +489,45 @@ class SecurityComponent extends Component
         $unlockFieldsMessages = $this->_debugCheckFields(
             $dataUnlockedFields,
             $expectedUnlockedFields,
-            'On request data field \'%s\', was not expected as unlocked',
+            'Unexpected unlocked field \'%s\' in POST data',
             null,
-            'On request data there were missing expected unlocked fields: \'%s\''
+            'Missing unlocked field: \'%s\''
         );
 
-        $messages = array_merge($fieldsMessages, $unlockFieldsMessages);
+        $messages = array_merge($messages, $fieldsMessages, $unlockFieldsMessages);
         return implode(', ', $messages);
     }
 
     /**
      * Iterates data array to check against expected
-     * @param $dataFields
-     * @param $expectedFields
-     * @param $intKeyMessage
-     * @param $stringKeyMessage
-     * @param $missingMessage
+     *
+     * @param array $dataFields Fields array, containing the POST data fields
+     * @param array $expectedFields Fields array, containing the expected fields we should have in POST
+     * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected)
+     * @param string $stringKeyMessage Message string if tampered found in data fields indexed by string (protected)
+     * @param string $missingMessage Message string if missing field
      * @return array Messages
      */
-    protected function _debugCheckFields($dataFields, $expectedFields, $intKeyMessage = '', $stringKeyMessage = '', $missingMessage = '')
+    protected function _debugCheckFields($dataFields, $expectedFields = [], $intKeyMessage = '', $stringKeyMessage = '', $missingMessage = '')
     {
         $messages = [];
         foreach ($dataFields as $key => $value) {
             if (is_int($key)) {
-                $foundKey = array_search($value, $expectedFields);
+                $foundKey = array_search($value, (array)$expectedFields);
                 if ($foundKey === false) {
                     $messages[] = sprintf($intKeyMessage, $value);
                 } else {
                     unset($expectedFields[$foundKey]);
                 }
             } elseif (is_string($key)) {
-                if ($value !== $expectedFields[$key]) {
+                if (isset($expectedFields[$key]) && $value !== $expectedFields[$key]) {
                     $messages[] = sprintf($stringKeyMessage, $key, $expectedFields[$key], $value);
                 }
                 unset($expectedFields[$key]);
             }
         }
         if (count($expectedFields) > 0) {
-            $messages[] = sprintf($missingMessage, implode('\', \'', $expectedFields));
+            $messages[] = sprintf($missingMessage, implode(', ', $expectedFields));
         }
         return $messages;
     }
@@ -554,7 +563,7 @@ class SecurityComponent extends Component
     /**
      * Calls a controller callback method
      *
-     * @param \Cake\Controller\Controller $controller Controller to run callback on
+     * @param \Cake\Controller\Controller $controller Instantiating controller
      * @param string $method Method to execute
      * @param array $params Parameters to send to method
      * @return mixed Controller callback method's response

+ 4 - 0
src/Controller/Exception/AuthSecurityException.php

@@ -17,5 +17,9 @@ namespace Cake\Controller\Exception;
  */
 class AuthSecurityException extends SecurityException
 {
+    /**
+     * Security Exception type
+     * @var string
+     */
     public $type = 'auth';
 }

+ 3 - 1
src/Controller/Exception/SecurityException.php

@@ -27,9 +27,11 @@ class SecurityException extends BadRequestException
 
     /**
      * Getter for type
+     *
      * @return string
      */
-    public function getType() {
+    public function getType()
+    {
         return $this->_type;
     }
 }

+ 459 - 71
tests/TestCase/Controller/Component/SecurityComponentTest.php

@@ -16,8 +16,10 @@ namespace Cake\Test\TestCase\Controller\Component;
 
 use Cake\Controller\Component\SecurityComponent;
 use Cake\Controller\Controller;
+use Cake\Controller\Exception\SecurityException;
 use Cake\Core\Configure;
 use Cake\Event\Event;
+use Cake\Network\Exception\BadRequestException;
 use Cake\Network\Request;
 use Cake\Network\Session;
 use Cake\TestSuite\TestCase;
@@ -40,6 +42,17 @@ class TestSecurityComponent extends SecurityComponent
     {
         return $this->_validatePost($controller);
     }
+
+    /**
+     * authRequired method
+     *
+     * @param Controller $controller
+     * @return bool
+     */
+    public function authRequired(Controller $controller)
+    {
+        return $this->_authRequired($controller);
+    }
 }
 
 /**
@@ -110,6 +123,8 @@ class SecurityTestController extends Controller
 /**
  * SecurityComponentTest class
  *
+ * @property SecurityComponent Security
+ * @property SecurityTestController Controller
  */
 class SecurityComponentTest extends TestCase
 {
@@ -167,6 +182,17 @@ class SecurityComponentTest extends TestCase
         unset($this->Controller);
     }
 
+    public function validatePost($expectedException = null, $expectedExceptionMessage = null)
+    {
+        try {
+            return $this->Controller->Security->validatePost($this->Controller);
+        } catch (SecurityException $ex) {
+            $this->assertInstanceOf('Cake\\Controller\\Exception\\' . $expectedException, $ex);
+            $this->assertEquals($expectedExceptionMessage, $ex->getMessage());
+            return false;
+        }
+    }
+
     /**
      * Test that requests are still blackholed when controller has incorrect
      * visibility keyword in the blackhole callback
@@ -389,12 +415,13 @@ class SecurityComponentTest extends TestCase
 
         $fields = '68730b0747d4889ec2766f9117405f9635f5fd5e%3AModel.valid';
         $unlocked = '';
+        $debug = '';
 
         $this->Controller->request->data = [
             'Model' => ['username' => 'nate', 'password' => 'foo', 'valid' => '0'],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
-        $this->assertTrue($this->Controller->Security->validatePost($this->Controller));
+        $this->assertTrue($this->validatePost());
     }
 
     /**
@@ -411,11 +438,16 @@ class SecurityComponentTest extends TestCase
 
         $fields = 'an-invalid-token';
         $unlocked = '';
+        $debug = urlencode(json_encode([
+            'some-action',
+            [],
+            []
+        ]));
 
         $this->Controller->request->env('REQUEST_METHOD', 'GET');
         $this->Controller->request->data = [
             'Model' => ['username' => 'nate', 'password' => 'foo', 'valid' => '0'],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug')
         ];
         $this->Controller->Security->startup($event);
         $this->assertTrue($this->Controller->failed);
@@ -432,6 +464,33 @@ class SecurityComponentTest extends TestCase
         $event = new Event('Controller.startup', $this->Controller);
         $this->Controller->Security->startup($event);
         $this->Security->session->delete('_Token');
+        $unlocked = '';
+        $debug = urlencode(json_encode([
+            '/articles/index',
+            [],
+            []
+        ]));
+
+        $fields = 'a5475372b40f6e3ccbf9f8af191f20e1642fd877%3AModel.valid';
+
+        $this->Controller->request->data = [
+            'Model' => ['username' => 'nate', 'password' => 'foo', 'valid' => '0'],
+            '_Token' => compact('fields', 'unlocked', 'debug')
+        ];
+        $this->assertFalse($this->validatePost('SecurityException', 'Unexpected field \'Model.password\' in POST data, Unexpected field \'Model.username\' in POST data'));
+    }
+
+    /**
+     * Test that validatePost fails if you are missing unlocked in request data.
+     *
+     * @return void
+     * @triggers Controller.startup $this->Controller
+     */
+    public function testValidatePostNoUnlockedInRequestData()
+    {
+        $event = new Event('Controller.startup', $this->Controller);
+        $this->Controller->Security->startup($event);
+        $this->Security->session->delete('_Token');
 
         $fields = 'a5475372b40f6e3ccbf9f8af191f20e1642fd877%3AModel.valid';
 
@@ -439,7 +498,7 @@ class SecurityComponentTest extends TestCase
             'Model' => ['username' => 'nate', 'password' => 'foo', 'valid' => '0'],
             '_Token' => compact('fields')
         ];
-        $this->assertFalse($this->Controller->Security->validatePost($this->Controller));
+        $this->assertFalse($this->validatePost('SecurityException', '\'_Token.unlocked\' was not found in request data.'));
     }
 
     /**
@@ -458,7 +517,7 @@ class SecurityComponentTest extends TestCase
             'Model' => ['username' => 'nate', 'password' => 'foo', 'valid' => '0'],
             '_Token' => compact('unlocked')
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost('SecurityException', '\'_Token.fields\' was not found in request data.');
         $this->assertFalse($result, 'validatePost passed when fields were missing. %s');
     }
 
@@ -475,6 +534,11 @@ class SecurityComponentTest extends TestCase
         $this->Controller->Security->startup($event);
         $fields = 'a5475372b40f6e3ccbf9f8af191f20e1642fd877';
         $unlocked = '';
+        $debug = urlencode(json_encode([
+            '/articles/index',
+            ['Model.password', 'Model.username', 'Model.valid'],
+            []
+        ]));
 
         // a corrupted serialized object, so we can see if it ever gets to deserialize
         $attack = 'O:3:"App":1:{s:5:"__map";a:1:{s:3:"foo";s:7:"Hacked!";s:1:"fail"}}';
@@ -482,9 +546,9 @@ class SecurityComponentTest extends TestCase
 
         $this->Controller->request->data = [
             'Model' => ['username' => 'mark', 'password' => 'foo', 'valid' => '0'],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug')
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost('SecurityException', 'Bad Request');
         $this->assertFalse($result, 'validatePost passed when key was missing. %s');
     }
 
@@ -501,13 +565,14 @@ class SecurityComponentTest extends TestCase
 
         $fields = '8e26ef05379e5402c2c619f37ee91152333a0264%3A';
         $unlocked = '';
+        $debug = 'not used';
 
         $this->Controller->request->data = [
             '_csrfToken' => 'abc123',
             'Model' => ['multi_field' => ['1', '3']],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug')
         ];
-        $this->assertTrue($this->Controller->Security->validatePost($this->Controller));
+        $this->assertTrue($this->validatePost());
     }
 
     /**
@@ -523,18 +588,23 @@ class SecurityComponentTest extends TestCase
 
         $fields = '8e26ef05379e5402c2c619f37ee91152333a0264%3A';
         $unlocked = '';
+        $debug = urlencode(json_encode([
+            'some-action',
+            [],
+            []
+        ]));
 
         $this->Controller->request->data = [
             'Model' => ['multi_field' => ['1', '3']],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug')
         ];
-        $this->assertTrue($this->Controller->Security->validatePost($this->Controller));
+        $this->assertTrue($this->validatePost());
 
         $this->Controller->request->data = [
             'Model' => ['multi_field' => [12 => '1', 20 => '3']],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug')
         ];
-        $this->assertTrue($this->Controller->Security->validatePost($this->Controller));
+        $this->assertTrue($this->validatePost());
     }
 
     /**
@@ -549,12 +619,17 @@ class SecurityComponentTest extends TestCase
 
         $fields = '4a221010dd7a23f7166cb10c38bc21d81341c387%3A';
         $unlocked = '';
+        $debug = urlencode(json_encode([
+            'some-action',
+            [],
+            []
+        ]));
 
         $this->Controller->request->data = [
             1 => 'value,',
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug')
         ];
-        $this->assertTrue($this->Controller->Security->validatePost($this->Controller));
+        $this->assertTrue($this->validatePost());
     }
 
     /**
@@ -570,13 +645,14 @@ class SecurityComponentTest extends TestCase
 
         $fields = 'a1c3724b7ba85e7022413611e30ba2c6181d5aba%3A';
         $unlocked = '';
+        $debug = 'not used';
 
         $this->Controller->request->data = [
             'anything' => 'some_data',
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug')
         ];
 
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -593,13 +669,14 @@ class SecurityComponentTest extends TestCase
 
         $fields = 'b0914d06dfb04abf1fada53e16810e87d157950b%3A';
         $unlocked = '';
+        $debug = 'not used';
 
         $this->Controller->request->data = [
             'Model' => ['username' => '', 'password' => ''],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug')
         ];
 
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -616,6 +693,7 @@ class SecurityComponentTest extends TestCase
 
         $fields = 'b65c7463e44a61d8d2eaecce2c265b406c9c4742%3AAddresses.0.id%7CAddresses.1.id';
         $unlocked = '';
+        $debug = 'not used';
 
         $this->Controller->request->data = [
             'Addresses' => [
@@ -628,9 +706,9 @@ class SecurityComponentTest extends TestCase
                     'address' => '', 'city' => '', 'phone' => '', 'primary' => ''
                 ]
             ],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug')
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -647,35 +725,36 @@ class SecurityComponentTest extends TestCase
 
         $fields = '8d8da68ba03b3d6e7e145b948abfe26741422169%3A';
         $unlocked = '';
+        $debug = 'not used';
 
         $this->Controller->request->data = [
             'Tag' => ['Tag' => [1, 2]],
-            '_Token' => compact('fields', 'unlocked'),
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
 
         $this->Controller->request->data = [
             'Tag' => ['Tag' => [1, 2, 3]],
-            '_Token' => compact('fields', 'unlocked'),
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
 
         $this->Controller->request->data = [
             'Tag' => ['Tag' => [1, 2, 3, 4]],
-            '_Token' => compact('fields', 'unlocked'),
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
 
         $fields = 'eae2adda1628b771a30cc133342d16220c6520fe%3A';
         $this->Controller->request->data = [
             'User.password' => 'bar', 'User.name' => 'foo', 'User.is_valid' => '1',
             'Tag' => ['Tag' => [1]],
-            '_Token' => compact('fields', 'unlocked'),
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -694,23 +773,24 @@ class SecurityComponentTest extends TestCase
         $this->Controller->Security->startup($event);
         $fields = '68730b0747d4889ec2766f9117405f9635f5fd5e%3AModel.valid';
         $unlocked = '';
+        $debug = 'not used';
 
         $this->Controller->request->data = [
             'Model' => ['username' => '', 'password' => '', 'valid' => '0'],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
 
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
 
         $fields = 'f63e4a69b2edd31f064e8e602a04dd59307cfe9c%3A';
 
         $this->Controller->request->data = [
             'Model' => ['username' => '', 'password' => '', 'valid' => '0'],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
 
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
 
         $this->Controller->request->data = [];
@@ -718,10 +798,10 @@ class SecurityComponentTest extends TestCase
 
         $this->Controller->request->data = [
             'Model' => ['username' => '', 'password' => '', 'valid' => '0'],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
 
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -737,15 +817,16 @@ class SecurityComponentTest extends TestCase
         $this->Controller->Security->startup($event);
         $fields = '973a8939a68ac014cc6f7666cec9aa6268507350%3AModel.hidden%7CModel.other_hidden';
         $unlocked = '';
+        $debug = 'not used';
 
         $this->Controller->request->data = [
             'Model' => [
                 'username' => '', 'password' => '', 'hidden' => '0',
                 'other_hidden' => 'some hidden value'
             ],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -762,15 +843,16 @@ class SecurityComponentTest extends TestCase
         $this->Controller->Security->startup($event);
         $fields = '1c59acfbca98bd870c11fb544d545cbf23215880%3AModel.hidden';
         $unlocked = '';
+        $debug = 'not used';
 
         $this->Controller->request->data = [
             'Model' => [
                 'username' => '', 'password' => '', 'hidden' => '0'
             ],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
 
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -787,6 +869,7 @@ class SecurityComponentTest extends TestCase
         $unlocked = 'Model.username';
         $fields = ['Model.hidden', 'Model.password'];
         $fields = urlencode(Security::hash('/articles/index' . serialize($fields) . $unlocked . Security::salt()));
+        $debug = 'not used';
 
         $this->Controller->request->data = [
             'Model' => [
@@ -794,10 +877,10 @@ class SecurityComponentTest extends TestCase
                 'password' => 'sekret',
                 'hidden' => '0'
             ],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
 
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -823,8 +906,64 @@ class SecurityComponentTest extends TestCase
             '_Token' => compact('fields')
         ];
 
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost('SecurityException', '\'_Token.unlocked\' was not found in request data.');
+        $this->assertFalse($result);
+    }
+
+    /**
+     * test that missing 'debug' input causes failure
+     *
+     * @return void
+     * @triggers Controller.startup $this->Controller
+     */
+    public function testValidatePostFailNoDebug()
+    {
+        $event = new Event('Controller.startup', $this->Controller);
+        $this->Controller->Security->startup($event);
+        $fields = ['Model.hidden', 'Model.password', 'Model.username'];
+        $fields = urlencode(Security::hash(serialize($fields) . Security::salt()));
+        $unlocked = '';
+
+        $this->Controller->request->data = [
+            'Model' => [
+                'username' => 'mark',
+                'password' => 'sekret',
+                'hidden' => '0'
+            ],
+            '_Token' => compact('fields', 'unlocked')
+        ];
+
+        $result = $this->validatePost('SecurityException', '\'_Token.debug\' was not found in request data.');
+        $this->assertFalse($result);
+    }
+
+    /**
+     * test that missing 'debug' input is not the problem when debug mode disabled
+     *
+     * @return void
+     * @triggers Controller.startup $this->Controller
+     */
+    public function testValidatePostFailNoDebugMode()
+    {
+        $event = new Event('Controller.startup', $this->Controller);
+        $this->Controller->Security->startup($event);
+        $fields = ['Model.hidden', 'Model.password', 'Model.username'];
+        $fields = urlencode(Security::hash(serialize($fields) . Security::salt()));
+        $unlocked = '';
+
+        $this->Controller->request->data = [
+            'Model' => [
+                'username' => 'mark',
+                'password' => 'sekret',
+                'hidden' => '0'
+            ],
+            '_Token' => compact('fields', 'unlocked')
+        ];
+        $debug = Configure::read('debug');
+        Configure::write('debug', false);
+        $result = $this->validatePost();
         $this->assertFalse($result);
+        Configure::write('debug', $debug);
     }
 
     /**
@@ -840,6 +979,11 @@ class SecurityComponentTest extends TestCase
         $unlocked = 'Model.username';
         $fields = ['Model.hidden', 'Model.password'];
         $fields = urlencode(Security::hash(serialize($fields) . $unlocked . Security::salt()));
+        $debug = urlencode(json_encode([
+            '/articles/index',
+            ['Model.hidden', 'Model.password'],
+            ['Model.username']
+        ]));
 
         // Tamper the values.
         $unlocked = 'Model.username|Model.password';
@@ -850,10 +994,10 @@ class SecurityComponentTest extends TestCase
                 'password' => 'sekret',
                 'hidden' => '0'
             ],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug')
         ];
 
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost('SecurityException', 'Missing field \'Model.password\' in POST data, Unexpected unlocked field \'Model.password\' in POST data');
         $this->assertFalse($result);
     }
 
@@ -869,14 +1013,15 @@ class SecurityComponentTest extends TestCase
         $this->Controller->Security->startup($event);
         $fields = '075ca6c26c38a09a78d871201df89faf52cbbeb8%3AModel.valid%7CModel2.valid%7CModel3.valid';
         $unlocked = '';
+        $debug = 'not used';
 
         $this->Controller->request->data = [
             'Model' => ['username' => '', 'password' => '', 'valid' => '0'],
             'Model2' => ['valid' => '0'],
             'Model3' => ['valid' => '0'],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -893,6 +1038,7 @@ class SecurityComponentTest extends TestCase
         $fields = '24a753fb62ef7839389987b58e3f7108f564e529%3AModel.0.hidden%7CModel.0.valid';
         $fields .= '%7CModel.1.hidden%7CModel.1.valid';
         $unlocked = '';
+        $debug = 'not used';
 
         $this->Controller->request->data = [
             'Model' => [
@@ -905,10 +1051,10 @@ class SecurityComponentTest extends TestCase
                     'hidden' => 'value', 'valid' => '0'
                 ]
             ],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
 
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -925,6 +1071,7 @@ class SecurityComponentTest extends TestCase
         $fields = '8f7d82bf7656cf068822d9bdab109ebed1be1825%3AAddress.0.id%7CAddress.0.primary%7C';
         $fields .= 'Address.1.id%7CAddress.1.primary';
         $unlocked = '';
+        $debug = 'not used';
 
         $this->Controller->request->data = [
             'Address' => [
@@ -949,10 +1096,10 @@ class SecurityComponentTest extends TestCase
                     'primary' => '1'
                 ]
             ],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
 
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -969,15 +1116,16 @@ class SecurityComponentTest extends TestCase
         $unlocked = '';
         $hashFields = ['TaxonomyData'];
         $fields = urlencode(Security::hash('/articles/index' . serialize($hashFields) . $unlocked . Security::salt()));
+        $debug = 'not used';
 
         $this->Controller->request->data = [
             'TaxonomyData' => [
                 1 => [[2]],
                 2 => [[3]]
             ],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -996,6 +1144,28 @@ class SecurityComponentTest extends TestCase
         $fields = '7a203edb3d345bbf38fe0dccae960da8842e11d7%3AAddress.0.id%7CAddress.0.primary%7C';
         $fields .= 'Address.1.id%7CAddress.1.primary';
         $unlocked = '';
+        $debug = urlencode(json_encode([
+            '/articles/index',
+            [
+                'Address.0.address',
+                'Address.0.city',
+                'Address.0.first_name',
+                'Address.0.last_name',
+                'Address.0.phone',
+                'Address.0.title',
+                'Address.1.address',
+                'Address.1.city',
+                'Address.1.first_name',
+                'Address.1.last_name',
+                'Address.1.phone',
+                'Address.1.title',
+                'Address.0.id' => '123',
+                'Address.0.primary' => '5',
+                'Address.1.id' => '124',
+                'Address.1.primary' => '1'
+            ],
+            []
+        ]));
 
         $this->Controller->request->data = [
             'Address' => [
@@ -1020,10 +1190,9 @@ class SecurityComponentTest extends TestCase
                     'primary' => '1'
                 ]
             ],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
-
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost('SecurityException', 'Bad Request');
         $this->assertFalse($result);
     }
 
@@ -1040,12 +1209,17 @@ class SecurityComponentTest extends TestCase
         $this->Controller->Security->startup($event);
         $fields = '9da2b3fa2b5b8ac0bfbc1bbce145e58059629125%3An%3A0%3A%7B%7D';
         $unlocked = '';
+        $debug = urlencode(json_encode([
+            '/articles/index',
+            [],
+            []
+        ]));
 
         $this->Controller->request->data = [
             'MyModel' => ['name' => 'some data'],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost('SecurityException', 'Unexpected field \'MyModel.name\' in POST data');
         $this->assertFalse($result);
 
         $this->Controller->Security->startup($event);
@@ -1053,10 +1227,10 @@ class SecurityComponentTest extends TestCase
 
         $this->Controller->request->data = [
             'MyModel' => ['name' => 'some data'],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
 
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -1072,32 +1246,37 @@ class SecurityComponentTest extends TestCase
         $this->Controller->Security->startup($event);
         $fields = 'c2226a8879c3f4b513691295fc2519a29c44c8bb%3An%3A0%3A%7B%7D';
         $unlocked = '';
+        $debug = urlencode(json_encode([
+            '/articles/index',
+            [],
+            []
+        ]));
 
         $this->Controller->request->data = [
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug'),
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost('SecurityException', 'Bad Request');
         $this->assertFalse($result);
 
         $this->Controller->request->data = [
-            '_Token' => compact('fields', 'unlocked'),
+            '_Token' => compact('fields', 'unlocked', 'debug'),
             'Test' => ['test' => '']
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
 
         $this->Controller->request->data = [
-            '_Token' => compact('fields', 'unlocked'),
+            '_Token' => compact('fields', 'unlocked', 'debug'),
             'Test' => ['test' => '1']
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
 
         $this->Controller->request->data = [
-            '_Token' => compact('fields', 'unlocked'),
+            '_Token' => compact('fields', 'unlocked', 'debug'),
             'Test' => ['test' => '2']
         ];
-        $result = $this->Controller->Security->validatePost($this->Controller);
+        $result = $this->validatePost();
         $this->assertTrue($result);
     }
 
@@ -1114,12 +1293,18 @@ class SecurityComponentTest extends TestCase
 
         $fields = 'b0914d06dfb04abf1fada53e16810e87d157950b%3A';
         $unlocked = '';
+        $debug = urlencode(json_encode([
+            'another-url',
+            ['Model.username', 'Model.password'],
+            []
+        ]));
+
 
         $this->Controller->request->data = [
             'Model' => ['username' => '', 'password' => ''],
-            '_Token' => compact('fields', 'unlocked')
+            '_Token' => compact('fields', 'unlocked', 'debug')
         ];
-        $this->assertTrue($this->Security->validatePost($this->Controller));
+        $this->assertTrue($this->validatePost());
 
         $request = $this->getMock('Cake\Network\Request', ['here']);
         $request->expects($this->at(0))
@@ -1131,8 +1316,8 @@ class SecurityComponentTest extends TestCase
         $request->data = $this->Controller->request->data;
         $this->Controller->request = $request;
 
-        $this->assertFalse($this->Security->validatePost($this->Controller));
-        $this->assertFalse($this->Security->validatePost($this->Controller));
+        $this->assertFalse($this->validatePost('SecurityException', 'URL mismatch in POST data (expected \'another-url\' but found \'/posts/index?page=1\')'));
+        $this->assertFalse($this->validatePost('SecurityException', 'URL mismatch in POST data (expected \'another-url\' but found \'/posts/edit/1\')'));
     }
 
     /**
@@ -1181,4 +1366,207 @@ class SecurityComponentTest extends TestCase
         $result = $this->Controller->Security->startup($event);
         $this->assertNull($result);
     }
+
+    /**
+     * Test that debug token format is right
+     *
+     * @return void
+     * @triggers Controller.startup $this->Controller
+     */
+    public function testValidatePostDebugFormat()
+    {
+        $event = new Event('Controller.startup', $this->Controller);
+        $this->Controller->Security->startup($event);
+        $unlocked = 'Model.username';
+        $fields = ['Model.hidden', 'Model.password'];
+        $fields = urlencode(Security::hash(serialize($fields) . $unlocked . Security::salt()));
+        $debug = urlencode(json_encode([
+            '/articles/index',
+            ['Model.hidden', 'Model.password'],
+            ['Model.username'],
+            ['not expected']
+        ]));
+
+        $this->Controller->request->data = [
+            'Model' => [
+                'username' => 'mark',
+                'password' => 'sekret',
+                'hidden' => '0'
+            ],
+            '_Token' => compact('fields', 'unlocked', 'debug')
+        ];
+
+        $result = $this->validatePost('SecurityException', 'Invalid security debug token.');
+        $this->assertFalse($result);
+
+        $debug = urlencode(json_encode('not an array'));
+        $result = $this->validatePost('SecurityException', 'Invalid security debug token.');
+        $this->assertFalse($result);
+    }
+
+    /**
+     * test blackhole will now throw passed exception if debug enabled
+     *
+     * @expectedException Cake\Controller\Exception\SecurityException
+     * @expectedExceptionMessage error description
+     * @return void
+     */
+    public function testBlackholeThrowsException()
+    {
+        $this->Security->config('blackHoleCallback', '');
+        $this->Security->blackHole($this->Controller, 'auth', new SecurityException('error description'));
+    }
+
+    /**
+     * test blackhole will throw BadRequest if debug disabled
+     *
+     * @return void
+     */
+    public function testBlackholeThrowsBadRequest()
+    {
+        $this->Security->config('blackHoleCallback', '');
+        $debug = Configure::read('debug');
+        $message = '';
+
+        Configure::write('debug', false);
+        try {
+            $this->Security->blackHole($this->Controller, 'auth', new SecurityException('error description'));
+        } catch (BadRequestException $ex) {
+            $message = $ex->getMessage();
+        }
+        Configure::write('debug', $debug);
+        $this->assertEquals('The request has been black-holed', $message);
+    }
+
+    /**
+     * Test that validatePost fails with tampered fields and explanation
+     *
+     * @return void
+     * @triggers Controller.startup $this->Controller
+     */
+    public function testValidatePostFailTampering()
+    {
+        $event = new Event('Controller.startup', $this->Controller);
+        $this->Controller->Security->startup($event);
+        $unlocked = '';
+        $fields = ['Model.hidden' => 'value', 'Model.id' => '1'];
+        $debug = urlencode(json_encode([
+            '/articles/index',
+            $fields,
+            []
+        ]));
+        $fields = urlencode(Security::hash(serialize($fields) . $unlocked . Security::salt()));
+        $fields .= urlencode(':Model.hidden|Model.id');
+        $this->Controller->request->data = [
+            'Model' => [
+                'hidden' => 'tampered',
+                'id' => '1',
+            ],
+            '_Token' => compact('fields', 'unlocked', 'debug')
+        ];
+
+        $result = $this->validatePost('SecurityException', 'Tampered field \'Model.hidden\' in POST data (expected value \'value\' but found \'tampered\')');
+        $this->assertFalse($result);
+    }
+
+    /**
+     * Auth required throws exception token not found
+     *
+     * @return void
+     * @expectedException Cake\Controller\Exception\AuthSecurityException
+     * @expectedExceptionMessage '_Token' was not found in request data.
+     * @triggers Controller.startup $this->Controller
+     */
+    public function testAuthRequiredThrowsExceptionTokenNotFoundPost()
+    {
+        $this->Controller->Security->config('requireAuth', ['protected']);
+        $this->Controller->request->params['action'] = 'protected';
+        $this->Controller->request->data = 'notEmpty';
+        $event = new Event('Controller.startup', $this->Controller);
+        $this->Controller->Security->startup($event);
+        $this->Controller->Security->authRequired($this->Controller);
+    }
+
+    /**
+     * Auth required throws exception token not found in Session
+     *
+     * @return void
+     * @expectedException Cake\Controller\Exception\AuthSecurityException
+     * @expectedExceptionMessage '_Token' was not found in session.
+     * @triggers Controller.startup $this->Controller
+     */
+    public function testAuthRequiredThrowsExceptionTokenNotFoundSession()
+    {
+        $this->Controller->Security->config('requireAuth', ['protected']);
+        $this->Controller->request->params['action'] = 'protected';
+        $this->Controller->request->data = ['_Token' => 'not empty'];
+        $event = new Event('Controller.startup', $this->Controller);
+        $this->Controller->Security->startup($event);
+        $this->Controller->Security->authRequired($this->Controller);
+    }
+
+    /**
+     * Auth required throws exception controller not allowed
+     *
+     * @return void
+     * @expectedException Cake\Controller\Exception\AuthSecurityException
+     * @expectedExceptionMessage Controller 'NotAllowed' was not found in allowed controllers: 'Allowed, AnotherAllowed'.
+     * @triggers Controller.startup $this->Controller
+     */
+    public function testAuthRequiredThrowsExceptionControllerNotAllowed()
+    {
+        $this->Controller->Security->config('requireAuth', ['protected']);
+        $this->Controller->request->params['controller'] = 'NotAllowed';
+        $this->Controller->request->params['action'] = 'protected';
+        $this->Controller->request->data = ['_Token' => 'not empty'];
+        $this->Controller->request->session()->write('_Token', [
+            'allowedControllers' => ['Allowed', 'AnotherAllowed']
+        ]);
+        $event = new Event('Controller.startup', $this->Controller);
+        $this->Controller->Security->startup($event);
+        $this->Controller->Security->authRequired($this->Controller);
+    }
+
+    /**
+     * Auth required throws exception controller not allowed
+     *
+     * @return void
+     * @expectedException Cake\Controller\Exception\AuthSecurityException
+     * @expectedExceptionMessage Action 'NotAllowed::protected' was not found in allowed actions: 'index, view'.
+     * @triggers Controller.startup $this->Controller
+     */
+    public function testAuthRequiredThrowsExceptionActionNotAllowed()
+    {
+        $this->Controller->Security->config('requireAuth', ['protected']);
+        $this->Controller->request->params['controller'] = 'NotAllowed';
+        $this->Controller->request->params['action'] = 'protected';
+        $this->Controller->request->data = ['_Token' => 'not empty'];
+        $this->Controller->request->session()->write('_Token', [
+            'allowedActions' => ['index', 'view']
+        ]);
+        $event = new Event('Controller.startup', $this->Controller);
+        $this->Controller->Security->startup($event);
+        $this->Controller->Security->authRequired($this->Controller);
+    }
+
+    /**
+     * Auth required throws exception controller not allowed
+     *
+     * @return void
+     * @triggers Controller.startup $this->Controller
+     */
+    public function testAuthRequired()
+    {
+        $this->Controller->Security->config('requireAuth', ['protected']);
+        $this->Controller->request->params['controller'] = 'Allowed';
+        $this->Controller->request->params['action'] = 'protected';
+        $this->Controller->request->data = ['_Token' => 'not empty'];
+        $this->Controller->request->session()->write('_Token', [
+            'allowedActions' => ['protected'],
+            'allowedControllers' => ['Allowed'],
+        ]);
+        $event = new Event('Controller.startup', $this->Controller);
+        $this->Controller->Security->startup($event);
+        $this->assertTrue($this->Controller->Security->authRequired($this->Controller));
+    }
 }