CsrfProtectionMiddlewareTest.php 11 KB

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