FormProtectionComponentTest.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 4.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Controller\Component;
  17. use Cake\Controller\Component\FormProtectionComponent;
  18. use Cake\Controller\Controller;
  19. use Cake\Event\Event;
  20. use Cake\Http\Exception\BadRequestException;
  21. use Cake\Http\Exception\NotFoundException;
  22. use Cake\Http\Response;
  23. use Cake\Http\ServerRequest;
  24. use Cake\Http\Session;
  25. use Cake\TestSuite\TestCase;
  26. use Cake\Utility\Security;
  27. /**
  28. * FormProtectionComponentTest class
  29. */
  30. class FormProtectionComponentTest extends TestCase
  31. {
  32. /**
  33. * @var \Cake\Controller\Controller
  34. */
  35. protected $Controller;
  36. /**
  37. * @var \Cake\Controller\Component\FormProtectionComponent
  38. */
  39. protected $FormProtection;
  40. /**
  41. * setUp method
  42. *
  43. * Initializes environment state.
  44. *
  45. * @return void
  46. */
  47. public function setUp(): void
  48. {
  49. parent::setUp();
  50. $session = new Session();
  51. $session->id('cli');
  52. $request = new ServerRequest([
  53. 'url' => '/articles/index',
  54. 'session' => $session,
  55. 'params' => ['controller' => 'articles', 'action' => 'index'],
  56. ]);
  57. $this->Controller = new Controller($request);
  58. $this->Controller->loadComponent('FormProtection');
  59. $this->FormProtection = $this->Controller->FormProtection;
  60. Security::setSalt('foo!');
  61. }
  62. public function testConstructorSettingProperties(): void
  63. {
  64. $settings = [
  65. 'requireSecure' => ['update_account'],
  66. 'validatePost' => false,
  67. ];
  68. $FormProtection = new FormProtectionComponent($this->Controller->components(), $settings);
  69. $this->assertEquals($FormProtection->validatePost, $settings['validatePost']);
  70. }
  71. public function testValidation(): void
  72. {
  73. $fields = '4697b45f7f430ff3ab73018c20f315eecb0ba5a6%3AModel.valid';
  74. $unlocked = '';
  75. $debug = '';
  76. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody([
  77. 'Model' => ['username' => 'nate', 'password' => 'foo', 'valid' => '0'],
  78. '_Token' => compact('fields', 'unlocked', 'debug'),
  79. ]));
  80. $event = new Event('Controller.startup', $this->Controller);
  81. $this->assertNull($this->FormProtection->startup($event));
  82. }
  83. public function testValidationOnGetWithData(): void
  84. {
  85. $fields = 'an-invalid-token';
  86. $unlocked = '';
  87. $debug = urlencode(json_encode([
  88. 'some-action',
  89. [],
  90. [],
  91. ]));
  92. $this->Controller->setRequest($this->Controller->getRequest()
  93. ->withEnv('REQUEST_METHOD', 'GET')
  94. ->withData('Model', ['username' => 'nate', 'password' => 'foo', 'valid' => '0'])
  95. ->withData('_Token', compact('fields', 'unlocked', 'debug')));
  96. $event = new Event('Controller.startup', $this->Controller);
  97. $this->expectException(BadRequestException::class);
  98. $this->FormProtection->startup($event);
  99. }
  100. public function testValidationNoSession(): void
  101. {
  102. $unlocked = '';
  103. $debug = urlencode(json_encode([
  104. '/articles/index',
  105. [],
  106. [],
  107. ]));
  108. $fields = 'a5475372b40f6e3ccbf9f8af191f20e1642fd877%3AModel.valid';
  109. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody([
  110. 'Model' => ['username' => 'nate', 'password' => 'foo', 'valid' => '0'],
  111. '_Token' => compact('fields', 'unlocked', 'debug'),
  112. ]));
  113. $event = new Event('Controller.startup', $this->Controller);
  114. $this->expectException(BadRequestException::class);
  115. $this->expectExceptionMessage('Unexpected field `Model.password` in POST data, Unexpected field `Model.username` in POST data');
  116. $this->FormProtection->startup($event);
  117. }
  118. public function testValidationEmptyForm(): void
  119. {
  120. $this->Controller->setRequest($this->Controller->getRequest()
  121. ->withEnv('REQUEST_METHOD', 'POST')
  122. ->withParsedBody([]));
  123. $event = new Event('Controller.startup', $this->Controller);
  124. $this->expectException(BadRequestException::class);
  125. $this->expectExceptionMessage('`_Token` was not found in request data.');
  126. $this->FormProtection->startup($event);
  127. }
  128. public function testValidationFailTampering(): void
  129. {
  130. $unlocked = '';
  131. $fields = ['Model.hidden' => 'value', 'Model.id' => '1'];
  132. $debug = urlencode(json_encode([
  133. '/articles/index',
  134. $fields,
  135. [],
  136. ]));
  137. $fields = urlencode(Security::hash(serialize($fields) . $unlocked . Security::getSalt()));
  138. $fields .= urlencode(':Model.hidden|Model.id');
  139. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody([
  140. 'Model' => [
  141. 'hidden' => 'tampered',
  142. 'id' => '1',
  143. ],
  144. '_Token' => compact('fields', 'unlocked', 'debug'),
  145. ]));
  146. $this->expectException(BadRequestException::class);
  147. $this->expectExceptionMessage('Tampered field `Model.hidden` in POST data (expected value `value` but found `tampered`)');
  148. $event = new Event('Controller.startup', $this->Controller);
  149. $this->FormProtection->startup($event);
  150. }
  151. public function testCallbackReturnResponse()
  152. {
  153. $this->FormProtection->setConfig('validationFailureCallback', function (BadRequestException $exception) {
  154. return new Response(['body' => 'from callback']);
  155. });
  156. $this->Controller->setRequest($this->Controller->getRequest()
  157. ->withEnv('REQUEST_METHOD', 'POST')
  158. ->withParsedBody([]));
  159. $event = new Event('Controller.startup', $this->Controller);
  160. $result = $this->FormProtection->startup($event);
  161. $this->assertInstanceOf(Response::class, $result);
  162. $this->assertSame('from callback', (string)$result->getBody());
  163. }
  164. public function testUnlockedActions(): void
  165. {
  166. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody(['data']));
  167. $this->FormProtection->setConfig('unlockedActions', ['index']);
  168. $event = new Event('Controller.startup', $this->Controller);
  169. $result = $this->Controller->FormProtection->startup($event);
  170. $this->assertNull($result);
  171. }
  172. public function testCallbackThrowsException(): void
  173. {
  174. $this->expectException(NotFoundException::class);
  175. $this->expectExceptionMessage('error description');
  176. $this->FormProtection->setConfig('validationFailureCallback', function (BadRequestException $exception) {
  177. throw new NotFoundException('error description');
  178. });
  179. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody(['data']));
  180. $event = new Event('Controller.startup', $this->Controller);
  181. $this->Controller->FormProtection->startup($event);
  182. }
  183. public function testSettingTokenDataAsRequestAttribute(): void
  184. {
  185. $event = new Event('Controller.startup', $this->Controller);
  186. $this->Controller->FormProtection->startup($event);
  187. $securityToken = $this->Controller->getRequest()->getAttribute('formTokenData');
  188. $this->assertNotEmpty($securityToken);
  189. $this->assertSame([], $securityToken['unlockedFields']);
  190. }
  191. public function testClearingOfTokenFromRequestData(): void
  192. {
  193. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody(['_Token' => 'data']));
  194. $this->FormProtection->setConfig('validate', false);
  195. $event = new Event('Controller.startup', $this->Controller);
  196. $this->Controller->FormProtection->startup($event);
  197. $this->assertSame([], $this->Controller->getRequest()->getParsedBody());
  198. }
  199. }