CsrfProtectionMiddlewareTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  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.5.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Test\TestCase\Http\Middleware;
  16. use Cake\Http\Middleware\CsrfProtectionMiddleware;
  17. use Cake\Http\Response;
  18. use Cake\Http\ServerRequest;
  19. use Cake\I18n\Time;
  20. use Cake\TestSuite\TestCase;
  21. use Laminas\Diactoros\Response as DiactorosResponse;
  22. use Laminas\Diactoros\Response\RedirectResponse;
  23. use Psr\Http\Message\ServerRequestInterface;
  24. /**
  25. * Test for CsrfProtection
  26. */
  27. class CsrfProtectionMiddlewareTest extends TestCase
  28. {
  29. /**
  30. * Data provider for HTTP method tests.
  31. *
  32. * HEAD and GET do not populate $_POST or request->data.
  33. *
  34. * @return array
  35. */
  36. public static function safeHttpMethodProvider()
  37. {
  38. return [
  39. ['GET'],
  40. ['HEAD'],
  41. ];
  42. }
  43. /**
  44. * Data provider for HTTP methods that can contain request bodies.
  45. *
  46. * @return array
  47. */
  48. public static function httpMethodProvider()
  49. {
  50. return [
  51. ['OPTIONS'], ['PATCH'], ['PUT'], ['POST'], ['DELETE'], ['PURGE'], ['INVALIDMETHOD'],
  52. ];
  53. }
  54. /**
  55. * Provides the callback for the next middleware
  56. *
  57. * @return callable
  58. */
  59. protected function _getNextClosure()
  60. {
  61. return function ($request, $response) {
  62. return $response;
  63. };
  64. }
  65. /**
  66. * Test setting the cookie value
  67. *
  68. * @return void
  69. */
  70. public function testSettingCookie()
  71. {
  72. $request = new ServerRequest([
  73. 'environment' => ['REQUEST_METHOD' => 'GET'],
  74. 'webroot' => '/dir/',
  75. ]);
  76. $response = new Response();
  77. $closure = function ($request, $response) {
  78. $cookie = $response->getCookie('csrfToken');
  79. $this->assertNotEmpty($cookie, 'Should set a token.');
  80. $this->assertRegExp('/^[a-f0-9]+$/', $cookie['value'], 'Should look like a hash.');
  81. $this->assertEquals(0, $cookie['expire'], 'session duration.');
  82. $this->assertEquals('/dir/', $cookie['path'], 'session path.');
  83. $this->assertEquals($cookie['value'], $request->getParam('_csrfToken'));
  84. };
  85. $middleware = new CsrfProtectionMiddleware();
  86. $middleware($request, $response, $closure);
  87. }
  88. /**
  89. * Test that the CSRF tokens are not required for idempotent operations
  90. *
  91. * @dataProvider safeHttpMethodProvider
  92. * @return void
  93. */
  94. public function testSafeMethodNoCsrfRequired($method)
  95. {
  96. $request = new ServerRequest([
  97. 'environment' => [
  98. 'REQUEST_METHOD' => $method,
  99. 'HTTP_X_CSRF_TOKEN' => 'nope',
  100. ],
  101. 'cookies' => ['csrfToken' => 'testing123'],
  102. ]);
  103. $response = new Response();
  104. // No exception means the test is valid
  105. $middleware = new CsrfProtectionMiddleware();
  106. $response = $middleware($request, $response, $this->_getNextClosure());
  107. $this->assertInstanceOf(Response::class, $response);
  108. }
  109. /**
  110. * Test that the X-CSRF-Token works with the various http methods.
  111. *
  112. * @dataProvider httpMethodProvider
  113. * @return void
  114. */
  115. public function testValidTokenInHeader($method)
  116. {
  117. $request = new ServerRequest([
  118. 'environment' => [
  119. 'REQUEST_METHOD' => $method,
  120. 'HTTP_X_CSRF_TOKEN' => 'testing123',
  121. ],
  122. 'post' => ['a' => 'b'],
  123. 'cookies' => ['csrfToken' => 'testing123'],
  124. ]);
  125. $response = new Response();
  126. // No exception means the test is valid
  127. $middleware = new CsrfProtectionMiddleware();
  128. $response = $middleware($request, $response, $this->_getNextClosure());
  129. $this->assertInstanceOf(Response::class, $response);
  130. }
  131. /**
  132. * Test that the X-CSRF-Token works with the various http methods.
  133. *
  134. * @dataProvider httpMethodProvider
  135. * @return void
  136. */
  137. public function testInvalidTokenInHeader($method)
  138. {
  139. $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class);
  140. $request = new ServerRequest([
  141. 'environment' => [
  142. 'REQUEST_METHOD' => $method,
  143. 'HTTP_X_CSRF_TOKEN' => 'nope',
  144. ],
  145. 'post' => ['a' => 'b'],
  146. 'cookies' => ['csrfToken' => 'testing123'],
  147. ]);
  148. $response = new Response();
  149. $middleware = new CsrfProtectionMiddleware();
  150. $middleware($request, $response, $this->_getNextClosure());
  151. }
  152. /**
  153. * Test that request data works with the various http methods.
  154. *
  155. * @dataProvider httpMethodProvider
  156. * @return void
  157. */
  158. public function testValidTokenRequestData($method)
  159. {
  160. $request = new ServerRequest([
  161. 'environment' => [
  162. 'REQUEST_METHOD' => $method,
  163. ],
  164. 'post' => ['_csrfToken' => 'testing123'],
  165. 'cookies' => ['csrfToken' => 'testing123'],
  166. ]);
  167. $response = new Response();
  168. $closure = function ($request, $response) {
  169. $this->assertNull($request->getData('_csrfToken'));
  170. };
  171. // No exception means everything is OK
  172. $middleware = new CsrfProtectionMiddleware();
  173. $middleware($request, $response, $closure);
  174. }
  175. /**
  176. * Test that request data works with the various http methods.
  177. *
  178. * @dataProvider httpMethodProvider
  179. * @return void
  180. */
  181. public function testInvalidTokenRequestData($method)
  182. {
  183. $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class);
  184. $request = new ServerRequest([
  185. 'environment' => [
  186. 'REQUEST_METHOD' => $method,
  187. ],
  188. 'post' => ['_csrfToken' => 'nope'],
  189. 'cookies' => ['csrfToken' => 'testing123'],
  190. ]);
  191. $response = new Response();
  192. $middleware = new CsrfProtectionMiddleware();
  193. $middleware($request, $response, $this->_getNextClosure());
  194. }
  195. /**
  196. * Test that missing post field fails
  197. *
  198. * @return void
  199. */
  200. public function testInvalidTokenRequestDataMissing()
  201. {
  202. $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class);
  203. $request = new ServerRequest([
  204. 'environment' => [
  205. 'REQUEST_METHOD' => 'POST',
  206. ],
  207. 'post' => [],
  208. 'cookies' => ['csrfToken' => 'testing123'],
  209. ]);
  210. $response = new Response();
  211. $middleware = new CsrfProtectionMiddleware();
  212. $middleware($request, $response, $this->_getNextClosure());
  213. }
  214. /**
  215. * Test that request data works with the various http methods.
  216. *
  217. * @return void
  218. */
  219. public function testInvalidTokenNonStringData()
  220. {
  221. $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class);
  222. $request = new ServerRequest([
  223. 'environment' => [
  224. 'REQUEST_METHOD' => 'POST',
  225. ],
  226. 'post' => ['_csrfToken' => ['nope']],
  227. 'cookies' => ['csrfToken' => ['nope']],
  228. ]);
  229. $response = new Response();
  230. $middleware = new CsrfProtectionMiddleware();
  231. $middleware($request, $response, $this->_getNextClosure());
  232. }
  233. /**
  234. * Test that missing header and cookie fails
  235. *
  236. * @dataProvider httpMethodProvider
  237. * @return void
  238. */
  239. public function testInvalidTokenMissingCookie($method)
  240. {
  241. $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class);
  242. $request = new ServerRequest([
  243. 'environment' => [
  244. 'REQUEST_METHOD' => $method,
  245. ],
  246. 'post' => ['_csrfToken' => 'could-be-valid'],
  247. 'cookies' => [],
  248. ]);
  249. $response = new Response();
  250. $middleware = new CsrfProtectionMiddleware();
  251. $middleware($request, $response, $this->_getNextClosure());
  252. }
  253. /**
  254. * Test that the configuration options work.
  255. *
  256. * @return void
  257. */
  258. public function testConfigurationCookieCreate()
  259. {
  260. $request = new ServerRequest([
  261. 'environment' => ['REQUEST_METHOD' => 'GET'],
  262. 'webroot' => '/dir/',
  263. ]);
  264. $response = new Response();
  265. $closure = function ($request, $response) {
  266. $this->assertEmpty($response->getCookie('csrfToken'));
  267. $cookie = $response->getCookie('token');
  268. $this->assertNotEmpty($cookie, 'Should set a token.');
  269. $this->assertRegExp('/^[a-f0-9]+$/', $cookie['value'], 'Should look like a hash.');
  270. $this->assertWithinRange((new Time('+1 hour'))->format('U'), $cookie['expire'], 1, 'session duration.');
  271. $this->assertEquals('/dir/', $cookie['path'], 'session path.');
  272. $this->assertTrue($cookie['secure'], 'cookie security flag missing');
  273. $this->assertTrue($cookie['httpOnly'], 'cookie httpOnly flag missing');
  274. };
  275. $middleware = new CsrfProtectionMiddleware([
  276. 'cookieName' => 'token',
  277. 'expiry' => '+1 hour',
  278. 'secure' => true,
  279. 'httpOnly' => true,
  280. ]);
  281. $middleware($request, $response, $closure);
  282. }
  283. /**
  284. * Test that the configuration options work.
  285. *
  286. * There should be no exception thrown.
  287. *
  288. * @return void
  289. */
  290. public function testConfigurationValidate()
  291. {
  292. $request = new ServerRequest([
  293. 'environment' => ['REQUEST_METHOD' => 'POST'],
  294. 'cookies' => ['csrfToken' => 'nope', 'token' => 'yes'],
  295. 'post' => ['_csrfToken' => 'no match', 'token' => 'yes'],
  296. ]);
  297. $response = new Response();
  298. $middleware = new CsrfProtectionMiddleware([
  299. 'cookieName' => 'token',
  300. 'field' => 'token',
  301. 'expiry' => 90,
  302. ]);
  303. $response = $middleware($request, $response, $this->_getNextClosure());
  304. $this->assertInstanceOf(Response::class, $response);
  305. }
  306. /**
  307. * @return void
  308. */
  309. public function testSkippingTokenCheckUsingWhitelistCallback()
  310. {
  311. $request = new ServerRequest([
  312. 'environment' => [
  313. 'REQUEST_METHOD' => 'POST',
  314. ],
  315. ]);
  316. $response = new Response();
  317. $middleware = new CsrfProtectionMiddleware();
  318. $middleware->whitelistCallback(function (ServerRequestInterface $request) {
  319. $this->assertSame('POST', $request->getServerParams()['REQUEST_METHOD']);
  320. return true;
  321. });
  322. $response = $middleware($request, $response, $this->_getNextClosure());
  323. $this->assertInstanceOf(Response::class, $response);
  324. }
  325. }