FormProtectionComponentTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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\Routing\Router;
  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 testValidationWithBaseUrl(): void
  85. {
  86. $session = new Session();
  87. $session->id('cli');
  88. $request = new ServerRequest([
  89. 'url' => '/articles/index',
  90. 'base' => '/subfolder',
  91. 'webroot' => '/subfolder/',
  92. 'session' => $session,
  93. 'params' => ['controller' => 'articles', 'action' => 'index'],
  94. ]);
  95. Router::setRequest($request);
  96. $this->Controller->setRequest($request);
  97. $unlocked = '';
  98. $fields = ['id' => '1'];
  99. $debug = urlencode(json_encode([
  100. '/subfolder/articles/index',
  101. $fields,
  102. [],
  103. ]));
  104. $fields = hash_hmac(
  105. 'sha1',
  106. '/subfolder/articles/index' . serialize($fields) . $unlocked . 'cli',
  107. Security::getSalt()
  108. );
  109. $fields .= urlencode(':id');
  110. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody([
  111. 'id' => '1',
  112. '_Token' => compact('fields', 'unlocked', 'debug'),
  113. ]));
  114. $event = new Event('Controller.startup', $this->Controller);
  115. $this->assertNull($this->FormProtection->startup($event));
  116. }
  117. public function testValidationOnGetWithData(): void
  118. {
  119. $fields = 'an-invalid-token';
  120. $unlocked = '';
  121. $debug = urlencode(json_encode([
  122. 'some-action',
  123. [],
  124. [],
  125. ]));
  126. $this->Controller->setRequest($this->Controller->getRequest()
  127. ->withEnv('REQUEST_METHOD', 'GET')
  128. ->withData('Model', ['username' => 'nate', 'password' => 'foo', 'valid' => '0'])
  129. ->withData('_Token', compact('fields', 'unlocked', 'debug')));
  130. $event = new Event('Controller.startup', $this->Controller);
  131. $this->expectException(BadRequestException::class);
  132. $this->FormProtection->startup($event);
  133. }
  134. public function testValidationNoSession(): void
  135. {
  136. $unlocked = '';
  137. $debug = urlencode(json_encode([
  138. '/articles/index',
  139. [],
  140. [],
  141. ]));
  142. $fields = 'a5475372b40f6e3ccbf9f8af191f20e1642fd877%3AModel.valid';
  143. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody([
  144. 'Model' => ['username' => 'nate', 'password' => 'foo', 'valid' => '0'],
  145. '_Token' => compact('fields', 'unlocked', 'debug'),
  146. ]));
  147. $event = new Event('Controller.startup', $this->Controller);
  148. $this->expectException(BadRequestException::class);
  149. $this->expectExceptionMessage('Unexpected field `Model.password` in POST data, Unexpected field `Model.username` in POST data');
  150. $this->FormProtection->startup($event);
  151. }
  152. public function testValidationEmptyForm(): void
  153. {
  154. $this->Controller->setRequest($this->Controller->getRequest()
  155. ->withEnv('REQUEST_METHOD', 'POST')
  156. ->withParsedBody([]));
  157. $event = new Event('Controller.startup', $this->Controller);
  158. $this->expectException(BadRequestException::class);
  159. $this->expectExceptionMessage('`_Token` was not found in request data.');
  160. $this->FormProtection->startup($event);
  161. }
  162. public function testValidationFailTampering(): void
  163. {
  164. $unlocked = '';
  165. $fields = ['Model.hidden' => 'value', 'Model.id' => '1'];
  166. $debug = urlencode(json_encode([
  167. '/articles/index',
  168. $fields,
  169. [],
  170. ]));
  171. $fields = hash_hmac('sha1', '/articles/index' . serialize($fields) . $unlocked . 'cli', Security::getSalt());
  172. $fields .= urlencode(':Model.hidden|Model.id');
  173. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody([
  174. 'Model' => [
  175. 'hidden' => 'tampered',
  176. 'id' => '1',
  177. ],
  178. '_Token' => compact('fields', 'unlocked', 'debug'),
  179. ]));
  180. $this->expectException(BadRequestException::class);
  181. $this->expectExceptionMessage('Tampered field `Model.hidden` in POST data (expected value `value` but found `tampered`)');
  182. $event = new Event('Controller.startup', $this->Controller);
  183. $this->FormProtection->startup($event);
  184. }
  185. public function testValidationUnlockedFieldsMismatch()
  186. {
  187. // Unlocked is empty when the token is created.
  188. $unlocked = '';
  189. $fields = ['open', 'title'];
  190. $debug = urlencode(json_encode([
  191. '/articles/index',
  192. $fields,
  193. [''],
  194. ]));
  195. $fields = hash_hmac('sha1', '/articles/index' . serialize($fields) . $unlocked . 'cli', Security::getSalt());
  196. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody([
  197. 'open' => 'yes',
  198. 'title' => 'yay',
  199. '_Token' => compact('fields', 'unlocked', 'debug'),
  200. ]));
  201. $this->expectException(BadRequestException::class);
  202. $this->expectExceptionMessage('Missing unlocked field');
  203. $event = new Event('Controller.startup', $this->Controller);
  204. $this->FormProtection->setConfig('unlockedFields', ['open']);
  205. $this->FormProtection->startup($event);
  206. }
  207. public function testValidationUnlockedFieldsSuccess()
  208. {
  209. $unlocked = 'open';
  210. $fields = ['title'];
  211. $debug = urlencode(json_encode([
  212. '/articles/index',
  213. $fields,
  214. ['open'],
  215. ]));
  216. $fields = hash_hmac('sha1', '/articles/index' . serialize($fields) . $unlocked . 'cli', Security::getSalt());
  217. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody([
  218. 'title' => 'yay',
  219. 'open' => 'yes',
  220. '_Token' => compact('fields', 'unlocked', 'debug'),
  221. ]));
  222. $event = new Event('Controller.startup', $this->Controller);
  223. $this->FormProtection->setConfig('unlockedFields', ['open']);
  224. $result = $this->FormProtection->startup($event);
  225. $this->assertNull($result);
  226. }
  227. public function testCallbackReturnResponse()
  228. {
  229. $this->FormProtection->setConfig('validationFailureCallback', function (BadRequestException $exception) {
  230. return new Response(['body' => 'from callback']);
  231. });
  232. $this->Controller->setRequest($this->Controller->getRequest()
  233. ->withEnv('REQUEST_METHOD', 'POST')
  234. ->withParsedBody([]));
  235. $event = new Event('Controller.startup', $this->Controller);
  236. $result = $this->FormProtection->startup($event);
  237. $this->assertInstanceOf(Response::class, $result);
  238. $this->assertSame('from callback', (string)$result->getBody());
  239. }
  240. public function testUnlockedActions(): void
  241. {
  242. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody(['data']));
  243. $this->FormProtection->setConfig('unlockedActions', ['index']);
  244. $event = new Event('Controller.startup', $this->Controller);
  245. $result = $this->Controller->FormProtection->startup($event);
  246. $this->assertNull($result);
  247. }
  248. public function testCallbackThrowsException(): void
  249. {
  250. $this->expectException(NotFoundException::class);
  251. $this->expectExceptionMessage('error description');
  252. $this->FormProtection->setConfig('validationFailureCallback', function (BadRequestException $exception) {
  253. throw new NotFoundException('error description');
  254. });
  255. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody(['data']));
  256. $event = new Event('Controller.startup', $this->Controller);
  257. $this->Controller->FormProtection->startup($event);
  258. }
  259. public function testSettingTokenDataAsRequestAttribute(): void
  260. {
  261. $event = new Event('Controller.startup', $this->Controller);
  262. $this->Controller->FormProtection->startup($event);
  263. $securityToken = $this->Controller->getRequest()->getAttribute('formTokenData');
  264. $this->assertNotEmpty($securityToken);
  265. $this->assertSame([], $securityToken['unlockedFields']);
  266. }
  267. public function testClearingOfTokenFromRequestData(): void
  268. {
  269. $this->Controller->setRequest($this->Controller->getRequest()->withParsedBody(['_Token' => 'data']));
  270. $this->FormProtection->setConfig('validate', false);
  271. $event = new Event('Controller.startup', $this->Controller);
  272. $this->Controller->FormProtection->startup($event);
  273. $this->assertSame([], $this->Controller->getRequest()->getParsedBody());
  274. }
  275. }