CsrfComponentTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Test\TestCase\Controller\Component;
  16. use Cake\Controller\ComponentRegistry;
  17. use Cake\Controller\Component\CsrfComponent;
  18. use Cake\Event\Event;
  19. use Cake\Http\Response;
  20. use Cake\Http\ServerRequest;
  21. use Cake\I18n\Time;
  22. use Cake\TestSuite\TestCase;
  23. /**
  24. * CsrfComponent test.
  25. */
  26. class CsrfComponentTest extends TestCase
  27. {
  28. /**
  29. * setup
  30. *
  31. * @return void
  32. */
  33. public function setUp()
  34. {
  35. parent::setUp();
  36. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  37. ->setMethods(['redirect'])
  38. ->getMock();
  39. $this->registry = new ComponentRegistry($controller);
  40. $this->component = new CsrfComponent($this->registry);
  41. }
  42. /**
  43. * teardown
  44. *
  45. * @return void
  46. */
  47. public function tearDown()
  48. {
  49. parent::tearDown();
  50. unset($this->component);
  51. }
  52. /**
  53. * Test setting the cookie value
  54. *
  55. * @return void
  56. * @triggers Controller.startup $controller
  57. */
  58. public function testSettingCookie()
  59. {
  60. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  61. ->setMethods(['redirect'])
  62. ->getMock();
  63. $controller->request = new ServerRequest([
  64. 'environment' => ['REQUEST_METHOD' => 'GET'],
  65. 'webroot' => '/dir/',
  66. ]);
  67. $controller->response = new Response();
  68. $event = new Event('Controller.startup', $controller);
  69. $this->component->startup($event);
  70. $cookie = $controller->response->cookie('csrfToken');
  71. $this->assertNotEmpty($cookie, 'Should set a token.');
  72. $this->assertRegExp('/^[a-f0-9]+$/', $cookie['value'], 'Should look like a hash.');
  73. $this->assertEquals(0, $cookie['expire'], 'session duration.');
  74. $this->assertEquals('/dir/', $cookie['path'], 'session path.');
  75. $this->assertEquals($cookie['value'], $controller->request->params['_csrfToken']);
  76. }
  77. /**
  78. * Data provider for HTTP method tests.
  79. *
  80. * HEAD and GET do not populate $_POST or request->data.
  81. *
  82. * @return array
  83. */
  84. public static function safeHttpMethodProvider()
  85. {
  86. return [
  87. ['GET'],
  88. ['HEAD'],
  89. ];
  90. }
  91. /**
  92. * Test that the CSRF tokens are not required for idempotent operations
  93. *
  94. * @dataProvider safeHttpMethodProvider
  95. * @return void
  96. */
  97. public function testSafeMethodNoCsrfRequired($method)
  98. {
  99. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  100. ->setMethods(['redirect'])
  101. ->getMock();
  102. $controller->request = new ServerRequest([
  103. 'environment' => [
  104. 'REQUEST_METHOD' => $method,
  105. 'HTTP_X_CSRF_TOKEN' => 'nope',
  106. ],
  107. 'cookies' => ['csrfToken' => 'testing123']
  108. ]);
  109. $controller->response = new Response();
  110. $event = new Event('Controller.startup', $controller);
  111. $result = $this->component->startup($event);
  112. $this->assertNull($result, 'No exception means valid.');
  113. }
  114. /**
  115. * Data provider for HTTP methods that can contain request bodies.
  116. *
  117. * @return array
  118. */
  119. public static function httpMethodProvider()
  120. {
  121. return [
  122. ['OPTIONS'], ['PATCH'], ['PUT'], ['POST'], ['DELETE'], ['PURGE'], ['INVALIDMETHOD']
  123. ];
  124. }
  125. /**
  126. * Test that the X-CSRF-Token works with the various http methods.
  127. *
  128. * @dataProvider httpMethodProvider
  129. * @return void
  130. * @triggers Controller.startup $controller
  131. */
  132. public function testValidTokenInHeader($method)
  133. {
  134. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  135. ->setMethods(['redirect'])
  136. ->getMock();
  137. $controller->request = new ServerRequest([
  138. 'environment' => [
  139. 'REQUEST_METHOD' => $method,
  140. 'HTTP_X_CSRF_TOKEN' => 'testing123',
  141. ],
  142. 'post' => ['a' => 'b'],
  143. 'cookies' => ['csrfToken' => 'testing123']
  144. ]);
  145. $controller->response = new Response();
  146. $event = new Event('Controller.startup', $controller);
  147. $result = $this->component->startup($event);
  148. $this->assertNull($result, 'No exception means valid.');
  149. }
  150. /**
  151. * Test that the X-CSRF-Token works with the various http methods.
  152. *
  153. * @dataProvider httpMethodProvider
  154. * @expectedException \Cake\Network\Exception\InvalidCsrfTokenException
  155. * @return void
  156. * @triggers Controller.startup $controller
  157. */
  158. public function testInvalidTokenInHeader($method)
  159. {
  160. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  161. ->setMethods(['redirect'])
  162. ->getMock();
  163. $controller->request = new ServerRequest([
  164. 'environment' => [
  165. 'REQUEST_METHOD' => $method,
  166. 'HTTP_X_CSRF_TOKEN' => 'nope',
  167. ],
  168. 'post' => ['a' => 'b'],
  169. 'cookies' => ['csrfToken' => 'testing123']
  170. ]);
  171. $controller->response = new Response();
  172. $event = new Event('Controller.startup', $controller);
  173. $this->component->startup($event);
  174. }
  175. /**
  176. * Test that request data works with the various http methods.
  177. *
  178. * @dataProvider httpMethodProvider
  179. * @return void
  180. * @triggers Controller.startup $controller
  181. */
  182. public function testValidTokenRequestData($method)
  183. {
  184. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  185. ->setMethods(['redirect'])
  186. ->getMock();
  187. $controller->request = new ServerRequest([
  188. 'environment' => [
  189. 'REQUEST_METHOD' => $method,
  190. ],
  191. 'post' => ['_csrfToken' => 'testing123'],
  192. 'cookies' => ['csrfToken' => 'testing123']
  193. ]);
  194. $controller->response = new Response();
  195. $event = new Event('Controller.startup', $controller);
  196. $result = $this->component->startup($event);
  197. $this->assertNull($result, 'No exception means valid.');
  198. $this->assertFalse(isset($controller->request->data['_csrfToken']));
  199. }
  200. /**
  201. * Test that request data works with the various http methods.
  202. *
  203. * @dataProvider httpMethodProvider
  204. * @expectedException \Cake\Network\Exception\InvalidCsrfTokenException
  205. * @return void
  206. */
  207. public function testInvalidTokenRequestData($method)
  208. {
  209. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  210. ->setMethods(['redirect'])
  211. ->getMock();
  212. $controller->request = new ServerRequest([
  213. 'environment' => [
  214. 'REQUEST_METHOD' => $method,
  215. ],
  216. 'post' => ['_csrfToken' => 'nope'],
  217. 'cookies' => ['csrfToken' => 'testing123']
  218. ]);
  219. $controller->response = new Response();
  220. $event = new Event('Controller.startup', $controller);
  221. $this->component->startup($event);
  222. }
  223. /**
  224. * Test that missing post field fails
  225. *
  226. * @expectedException \Cake\Network\Exception\InvalidCsrfTokenException
  227. * @return void
  228. */
  229. public function testInvalidTokenRequestDataMissing()
  230. {
  231. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  232. ->setMethods(['redirect'])
  233. ->getMock();
  234. $controller->request = new ServerRequest([
  235. 'environment' => [
  236. 'REQUEST_METHOD' => 'POST',
  237. ],
  238. 'post' => [],
  239. 'cookies' => ['csrfToken' => 'testing123']
  240. ]);
  241. $controller->response = new Response();
  242. $event = new Event('Controller.startup', $controller);
  243. $this->component->startup($event);
  244. }
  245. /**
  246. * Test that missing header and cookie fails
  247. *
  248. * @dataProvider httpMethodProvider
  249. * @expectedException \Cake\Network\Exception\InvalidCsrfTokenException
  250. * @return void
  251. */
  252. public function testInvalidTokenMissingCookie($method)
  253. {
  254. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  255. ->setMethods(['redirect'])
  256. ->getMock();
  257. $controller->request = new ServerRequest([
  258. 'environment' => [
  259. 'REQUEST_METHOD' => $method
  260. ],
  261. 'post' => ['_csrfToken' => 'could-be-valid'],
  262. 'cookies' => []
  263. ]);
  264. $controller->response = new Response();
  265. $event = new Event('Controller.startup', $controller);
  266. $this->component->startup($event);
  267. }
  268. /**
  269. * Test that CSRF checks are not applied to request action requests.
  270. *
  271. * @return void
  272. * @triggers Controller.startup $controller
  273. */
  274. public function testCsrfValidationSkipsRequestAction()
  275. {
  276. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  277. ->setMethods(['redirect'])
  278. ->getMock();
  279. $controller->request = new ServerRequest([
  280. 'environment' => ['REQUEST_METHOD' => 'POST'],
  281. 'params' => ['requested' => 1],
  282. 'post' => ['_csrfToken' => 'nope'],
  283. 'cookies' => ['csrfToken' => 'testing123']
  284. ]);
  285. $controller->response = new Response();
  286. $event = new Event('Controller.startup', $controller);
  287. $result = $this->component->startup($event);
  288. $this->assertNull($result, 'No error.');
  289. $this->assertEquals('testing123', $controller->request->params['_csrfToken']);
  290. }
  291. /**
  292. * Test that the configuration options work.
  293. *
  294. * @return void
  295. * @triggers Controller.startup $controller
  296. */
  297. public function testConfigurationCookieCreate()
  298. {
  299. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  300. ->setMethods(['redirect'])
  301. ->getMock();
  302. $controller->request = new ServerRequest([
  303. 'environment' => ['REQUEST_METHOD' => 'GET'],
  304. 'webroot' => '/dir/'
  305. ]);
  306. $controller->response = new Response();
  307. $component = new CsrfComponent($this->registry, [
  308. 'cookieName' => 'token',
  309. 'expiry' => '+1 hour',
  310. 'secure' => true,
  311. 'httpOnly' => true
  312. ]);
  313. $event = new Event('Controller.startup', $controller);
  314. $component->startup($event);
  315. $this->assertEmpty($controller->response->cookie('csrfToken'));
  316. $cookie = $controller->response->cookie('token');
  317. $this->assertNotEmpty($cookie, 'Should set a token.');
  318. $this->assertRegExp('/^[a-f0-9]+$/', $cookie['value'], 'Should look like a hash.');
  319. $this->assertWithinRange((new Time('+1 hour'))->format('U'), $cookie['expire'], 1, 'session duration.');
  320. $this->assertEquals('/dir/', $cookie['path'], 'session path.');
  321. $this->assertTrue($cookie['secure'], 'cookie security flag missing');
  322. $this->assertTrue($cookie['httpOnly'], 'cookie httpOnly flag missing');
  323. }
  324. /**
  325. * Test that the configuration options work.
  326. *
  327. * @return void
  328. * @triggers Controller.startup $controller
  329. */
  330. public function testConfigurationValidate()
  331. {
  332. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  333. ->setMethods(['redirect'])
  334. ->getMock();
  335. $controller->request = new ServerRequest([
  336. 'environment' => ['REQUEST_METHOD' => 'POST'],
  337. 'cookies' => ['csrfToken' => 'nope', 'token' => 'yes'],
  338. 'post' => ['_csrfToken' => 'no match', 'token' => 'yes'],
  339. ]);
  340. $controller->response = new Response();
  341. $component = new CsrfComponent($this->registry, [
  342. 'cookieName' => 'token',
  343. 'field' => 'token',
  344. 'expiry' => 90,
  345. ]);
  346. $event = new Event('Controller.startup', $controller);
  347. $result = $component->startup($event);
  348. $this->assertNull($result, 'Config settings should work.');
  349. }
  350. }