FormProtectionComponentTest.php 8.3 KB

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