CsrfProtectionMiddlewareTest.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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 missing header and cookie fails
  214. *
  215. * @dataProvider httpMethodProvider
  216. * @return void
  217. */
  218. public function testInvalidTokenMissingCookie($method)
  219. {
  220. $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class);
  221. $request = new ServerRequest([
  222. 'environment' => [
  223. 'REQUEST_METHOD' => $method
  224. ],
  225. 'post' => ['_csrfToken' => 'could-be-valid'],
  226. 'cookies' => []
  227. ]);
  228. $response = new Response();
  229. $middleware = new CsrfProtectionMiddleware();
  230. $middleware($request, $response, $this->_getNextClosure());
  231. }
  232. /**
  233. * Test that the configuration options work.
  234. *
  235. * @return void
  236. */
  237. public function testConfigurationCookieCreate()
  238. {
  239. $request = new ServerRequest([
  240. 'environment' => ['REQUEST_METHOD' => 'GET'],
  241. 'webroot' => '/dir/'
  242. ]);
  243. $response = new Response();
  244. $closure = function ($request, $response) {
  245. $this->assertEmpty($response->getCookie('csrfToken'));
  246. $cookie = $response->getCookie('token');
  247. $this->assertNotEmpty($cookie, 'Should set a token.');
  248. $this->assertRegExp('/^[a-f0-9]+$/', $cookie['value'], 'Should look like a hash.');
  249. $this->assertWithinRange((new Time('+1 hour'))->format('U'), $cookie['expire'], 1, 'session duration.');
  250. $this->assertEquals('/dir/', $cookie['path'], 'session path.');
  251. $this->assertTrue($cookie['secure'], 'cookie security flag missing');
  252. $this->assertTrue($cookie['httpOnly'], 'cookie httpOnly flag missing');
  253. };
  254. $middleware = new CsrfProtectionMiddleware([
  255. 'cookieName' => 'token',
  256. 'expiry' => '+1 hour',
  257. 'secure' => true,
  258. 'httpOnly' => true
  259. ]);
  260. $middleware($request, $response, $closure);
  261. }
  262. /**
  263. * Test that the configuration options work.
  264. *
  265. * There should be no exception thrown.
  266. *
  267. * @return void
  268. */
  269. public function testConfigurationValidate()
  270. {
  271. $request = new ServerRequest([
  272. 'environment' => ['REQUEST_METHOD' => 'POST'],
  273. 'cookies' => ['csrfToken' => 'nope', 'token' => 'yes'],
  274. 'post' => ['_csrfToken' => 'no match', 'token' => 'yes'],
  275. ]);
  276. $response = new Response();
  277. $middleware = new CsrfProtectionMiddleware([
  278. 'cookieName' => 'token',
  279. 'field' => 'token',
  280. 'expiry' => 90,
  281. ]);
  282. $response = $middleware($request, $response, $this->_getNextClosure());
  283. $this->assertInstanceOf(Response::class, $response);
  284. }
  285. /**
  286. * @return void
  287. */
  288. public function testSkippingTokenCheckUsingWhitelistCallback()
  289. {
  290. $request = new ServerRequest([
  291. 'environment' => [
  292. 'REQUEST_METHOD' => 'POST',
  293. ],
  294. ]);
  295. $response = new Response();
  296. $middleware = new CsrfProtectionMiddleware();
  297. $middleware->whitelistCallback(function (ServerRequestInterface $request) {
  298. $this->assertSame('POST', $request->getServerParams()['REQUEST_METHOD']);
  299. return true;
  300. });
  301. $response = $middleware($request, $response, $this->_getNextClosure());
  302. $this->assertInstanceOf(Response::class, $response);
  303. }
  304. }