CsrfProtectionMiddlewareTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
  12. * @link http://cakephp.org CakePHP(tm) Project
  13. * @since 3.5.0
  14. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Http\Middleware;
  17. use Cake\Http\Middleware\CsrfProtectionMiddleware;
  18. use Cake\Http\Response;
  19. use Cake\Http\ServerRequest;
  20. use Cake\I18n\Time;
  21. use Cake\TestSuite\TestCase;
  22. use Psr\Http\Message\ServerRequestInterface;
  23. use TestApp\Http\TestRequestHandler;
  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 request handler
  56. *
  57. * @return \Psr\Http\Server\RequestHandlerInterface
  58. */
  59. protected function _getRequestHandler()
  60. {
  61. return new TestRequestHandler(function ($request) {
  62. return new 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. $updatedRequest = null;
  77. $handler = new TestRequestHandler(function ($request) use (&$updatedRequest) {
  78. $updatedRequest = $request;
  79. return new Response();
  80. });
  81. $middleware = new CsrfProtectionMiddleware();
  82. $response = $middleware->process($request, $handler);
  83. $cookie = $response->getCookie('csrfToken');
  84. $this->assertNotEmpty($cookie, 'Should set a token.');
  85. $this->assertRegExp('/^[a-f0-9]+$/', $cookie['value'], 'Should look like a hash.');
  86. $this->assertSame(0, $cookie['expire'], 'session duration.');
  87. $this->assertSame('/dir/', $cookie['path'], 'session path.');
  88. $this->assertEquals($cookie['value'], $updatedRequest->getAttribute('csrfToken'));
  89. }
  90. /**
  91. * Test that the CSRF tokens are not required for idempotent operations
  92. *
  93. * @dataProvider safeHttpMethodProvider
  94. * @return void
  95. */
  96. public function testSafeMethodNoCsrfRequired($method)
  97. {
  98. $request = new ServerRequest([
  99. 'environment' => [
  100. 'REQUEST_METHOD' => $method,
  101. 'HTTP_X_CSRF_TOKEN' => 'nope',
  102. ],
  103. 'cookies' => ['csrfToken' => 'testing123'],
  104. ]);
  105. // No exception means the test is valid
  106. $middleware = new CsrfProtectionMiddleware();
  107. $response = $middleware->process($request, $this->_getRequestHandler());
  108. $this->assertInstanceOf(Response::class, $response);
  109. }
  110. /**
  111. * Test that the X-CSRF-Token works with the various http methods.
  112. *
  113. * @dataProvider httpMethodProvider
  114. * @return void
  115. */
  116. public function testValidTokenInHeader($method)
  117. {
  118. $request = new ServerRequest([
  119. 'environment' => [
  120. 'REQUEST_METHOD' => $method,
  121. 'HTTP_X_CSRF_TOKEN' => 'testing123',
  122. ],
  123. 'post' => ['a' => 'b'],
  124. 'cookies' => ['csrfToken' => 'testing123'],
  125. ]);
  126. $response = new Response();
  127. // No exception means the test is valid
  128. $middleware = new CsrfProtectionMiddleware();
  129. $response = $middleware->process($request, $this->_getRequestHandler());
  130. $this->assertInstanceOf(Response::class, $response);
  131. }
  132. /**
  133. * Test that the X-CSRF-Token works with the various http methods.
  134. *
  135. * @dataProvider httpMethodProvider
  136. * @return void
  137. */
  138. public function testInvalidTokenInHeader($method)
  139. {
  140. $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class);
  141. $request = new ServerRequest([
  142. 'environment' => [
  143. 'REQUEST_METHOD' => $method,
  144. 'HTTP_X_CSRF_TOKEN' => 'nope',
  145. ],
  146. 'post' => ['a' => 'b'],
  147. 'cookies' => ['csrfToken' => 'testing123'],
  148. ]);
  149. $response = new Response();
  150. $middleware = new CsrfProtectionMiddleware();
  151. $middleware->process($request, $this->_getRequestHandler());
  152. }
  153. /**
  154. * Test that request data works with the various http methods.
  155. *
  156. * @dataProvider httpMethodProvider
  157. * @return void
  158. */
  159. public function testValidTokenRequestData($method)
  160. {
  161. $request = new ServerRequest([
  162. 'environment' => [
  163. 'REQUEST_METHOD' => $method,
  164. ],
  165. 'post' => ['_csrfToken' => 'testing123'],
  166. 'cookies' => ['csrfToken' => 'testing123'],
  167. ]);
  168. $response = new Response();
  169. $handler = new TestRequestHandler(function ($request) {
  170. $this->assertNull($request->getData('_csrfToken'));
  171. return new Response();
  172. });
  173. // No exception means everything is OK
  174. $middleware = new CsrfProtectionMiddleware();
  175. $middleware->process($request, $handler);
  176. }
  177. /**
  178. * Test that request data works with the various http methods.
  179. *
  180. * @dataProvider httpMethodProvider
  181. * @return void
  182. */
  183. public function testInvalidTokenRequestData($method)
  184. {
  185. $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class);
  186. $request = new ServerRequest([
  187. 'environment' => [
  188. 'REQUEST_METHOD' => $method,
  189. ],
  190. 'post' => ['_csrfToken' => 'nope'],
  191. 'cookies' => ['csrfToken' => 'testing123'],
  192. ]);
  193. $response = new Response();
  194. $middleware = new CsrfProtectionMiddleware();
  195. $middleware->process($request, $this->_getRequestHandler());
  196. }
  197. /**
  198. * Test that missing post field fails
  199. *
  200. * @return void
  201. */
  202. public function testInvalidTokenRequestDataMissing()
  203. {
  204. $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class);
  205. $request = new ServerRequest([
  206. 'environment' => [
  207. 'REQUEST_METHOD' => 'POST',
  208. ],
  209. 'post' => [],
  210. 'cookies' => ['csrfToken' => 'testing123'],
  211. ]);
  212. $response = new Response();
  213. $middleware = new CsrfProtectionMiddleware();
  214. $middleware->process($request, $this->_getRequestHandler());
  215. }
  216. /**
  217. * Test that missing header and cookie fails
  218. *
  219. * @dataProvider httpMethodProvider
  220. * @return void
  221. */
  222. public function testInvalidTokenMissingCookie($method)
  223. {
  224. $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class);
  225. $request = new ServerRequest([
  226. 'environment' => [
  227. 'REQUEST_METHOD' => $method,
  228. ],
  229. 'post' => ['_csrfToken' => 'could-be-valid'],
  230. 'cookies' => [],
  231. ]);
  232. $response = new Response();
  233. $middleware = new CsrfProtectionMiddleware();
  234. $middleware->process($request, $this->_getRequestHandler());
  235. }
  236. /**
  237. * Test that the configuration options work.
  238. *
  239. * @return void
  240. */
  241. public function testConfigurationCookieCreate()
  242. {
  243. $request = new ServerRequest([
  244. 'environment' => ['REQUEST_METHOD' => 'GET'],
  245. 'webroot' => '/dir/',
  246. ]);
  247. $middleware = new CsrfProtectionMiddleware([
  248. 'cookieName' => 'token',
  249. 'expiry' => '+1 hour',
  250. 'secure' => true,
  251. 'httpOnly' => true,
  252. ]);
  253. $response = $middleware->process($request, $this->_getRequestHandler());
  254. $this->assertEmpty($response->getCookie('csrfToken'));
  255. $cookie = $response->getCookie('token');
  256. $this->assertNotEmpty($cookie, 'Should set a token.');
  257. $this->assertRegExp('/^[a-f0-9]+$/', $cookie['value'], 'Should look like a hash.');
  258. $this->assertWithinRange((new Time('+1 hour'))->format('U'), $cookie['expire'], 1, 'session duration.');
  259. $this->assertSame('/dir/', $cookie['path'], 'session path.');
  260. $this->assertTrue($cookie['secure'], 'cookie security flag missing');
  261. $this->assertTrue($cookie['httpOnly'], 'cookie httpOnly flag missing');
  262. }
  263. /**
  264. * Test that the configuration options work.
  265. *
  266. * There should be no exception thrown.
  267. *
  268. * @return void
  269. */
  270. public function testConfigurationValidate()
  271. {
  272. $request = new ServerRequest([
  273. 'environment' => ['REQUEST_METHOD' => 'POST'],
  274. 'cookies' => ['csrfToken' => 'nope', 'token' => 'yes'],
  275. 'post' => ['_csrfToken' => 'no match', 'token' => 'yes'],
  276. ]);
  277. $response = new Response();
  278. $middleware = new CsrfProtectionMiddleware([
  279. 'cookieName' => 'token',
  280. 'field' => 'token',
  281. 'expiry' => 90,
  282. ]);
  283. $response = $middleware->process($request, $this->_getRequestHandler());
  284. $this->assertInstanceOf(Response::class, $response);
  285. }
  286. /**
  287. * @return void
  288. */
  289. public function testSkippingTokenCheckUsingWhitelistCallback()
  290. {
  291. $request = new ServerRequest([
  292. 'post' => [
  293. '_csrfToken' => 'foo',
  294. ],
  295. 'environment' => [
  296. 'REQUEST_METHOD' => 'POST',
  297. ],
  298. ]);
  299. $response = new Response();
  300. $middleware = new CsrfProtectionMiddleware();
  301. $middleware->whitelistCallback(function (ServerRequestInterface $request) {
  302. $this->assertSame('POST', $request->getServerParams()['REQUEST_METHOD']);
  303. return true;
  304. });
  305. $handler = new TestRequestHandler(function ($request) {
  306. $this->assertEmpty($request->getParsedBody());
  307. return new Response();
  308. });
  309. $response = $middleware->process($request, $handler);
  310. $this->assertInstanceOf(Response::class, $response);
  311. }
  312. }