data. * * @return array */ public static function safeHttpMethodProvider() { return [ ['GET'], ['HEAD'], ]; } /** * Data provider for HTTP methods that can contain request bodies. * * @return array */ public static function httpMethodProvider() { return [ ['OPTIONS'], ['PATCH'], ['PUT'], ['POST'], ['DELETE'], ['PURGE'], ['INVALIDMETHOD'], ]; } /** * Provides the callback for the next middleware * * @return callable */ protected function _getNextClosure() { return function ($request, $response) { return $response; }; } /** * Test setting the cookie value * * @return void */ public function testSettingCookie() { $request = new ServerRequest([ 'environment' => ['REQUEST_METHOD' => 'GET'], 'webroot' => '/dir/', ]); $response = new Response(); $closure = function ($request, $response) { $cookie = $response->getCookie('csrfToken'); $this->assertNotEmpty($cookie, 'Should set a token.'); $this->assertRegExp('/^[a-f0-9]+$/', $cookie['value'], 'Should look like a hash.'); $this->assertEquals(0, $cookie['expire'], 'session duration.'); $this->assertEquals('/dir/', $cookie['path'], 'session path.'); $this->assertEquals($cookie['value'], $request->getParam('_csrfToken')); $this->assertRegExp('/^[a-z0-9]+$/', $cookie['value']); }; $middleware = new CsrfProtectionMiddleware(); $middleware($request, $response, $closure); } /** * Test that the CSRF tokens are not required for idempotent operations * * @dataProvider safeHttpMethodProvider * @return void */ public function testSafeMethodNoCsrfRequired($method) { $request = new ServerRequest([ 'environment' => [ 'REQUEST_METHOD' => $method, 'HTTP_X_CSRF_TOKEN' => 'nope', ], 'cookies' => ['csrfToken' => 'testing123'], ]); $response = new Response(); // No exception means the test is valid $middleware = new CsrfProtectionMiddleware(); $response = $middleware($request, $response, $this->_getNextClosure()); $this->assertInstanceOf(Response::class, $response); } /** * Test that the X-CSRF-Token works with the various http methods. * * @dataProvider httpMethodProvider * @return void */ public function testValidTokenInHeader($method) { $request = new ServerRequest([ 'environment' => [ 'REQUEST_METHOD' => $method, 'HTTP_X_CSRF_TOKEN' => 'testing123', ], 'post' => ['a' => 'b'], 'cookies' => ['csrfToken' => 'testing123'], ]); $response = new Response(); // No exception means the test is valid $middleware = new CsrfProtectionMiddleware(); $response = $middleware($request, $response, $this->_getNextClosure()); $this->assertInstanceOf(Response::class, $response); } /** * Test that the X-CSRF-Token works with the various http methods. * * @dataProvider httpMethodProvider * @return void */ public function testInvalidTokenInHeader($method) { $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class); $request = new ServerRequest([ 'environment' => [ 'REQUEST_METHOD' => $method, 'HTTP_X_CSRF_TOKEN' => 'nope', ], 'post' => ['a' => 'b'], 'cookies' => ['csrfToken' => 'testing123'], ]); $response = new Response(); $middleware = new CsrfProtectionMiddleware(); $middleware($request, $response, $this->_getNextClosure()); } /** * Test that request data works with the various http methods. * * @dataProvider httpMethodProvider * @return void */ public function testValidTokenRequestData($method) { $request = new ServerRequest([ 'environment' => [ 'REQUEST_METHOD' => $method, ], 'post' => ['_csrfToken' => 'testing123'], 'cookies' => ['csrfToken' => 'testing123'], ]); $response = new Response(); $closure = function ($request, $response) { $this->assertNull($request->getData('_csrfToken')); }; // No exception means everything is OK $middleware = new CsrfProtectionMiddleware(); $middleware($request, $response, $closure); } /** * Test that the X-CSRF-Token works with the various http methods. * * @dataProvider httpMethodProvider * @return void */ public function testValidTokenInHeaderVerifySource($method) { $middleware = new CsrfProtectionMiddleware(['verifyTokenSource' => true]); $token = $middleware->createToken(); $this->assertRegexp('/^[a-z0-9]+$/', $token, 'Token should not have unencoded binary data.'); $request = new ServerRequest([ 'environment' => [ 'REQUEST_METHOD' => $method, 'HTTP_X_CSRF_TOKEN' => $token, ], 'post' => ['a' => 'b'], 'cookies' => ['csrfToken' => $token], ]); $response = new Response(); // No exception means the test is valid $response = $middleware($request, $response, $this->_getNextClosure()); $this->assertInstanceOf(Response::class, $response); } /** * Test that the X-CSRF-Token works with the various http methods. * * @dataProvider httpMethodProvider * @return void */ public function testInvalidTokenInHeaderVerifySource($method) { $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class); $request = new ServerRequest([ 'environment' => [ 'REQUEST_METHOD' => $method, // Even though the values match they are not signed. 'HTTP_X_CSRF_TOKEN' => 'nope', ], 'post' => ['a' => 'b'], 'cookies' => ['csrfToken' => 'nope'], ]); $response = new Response(); $middleware = new CsrfProtectionMiddleware(['verifyTokenSource' => true]); $middleware($request, $response, $this->_getNextClosure()); } /** * Test that request data works with the various http methods. * * @dataProvider httpMethodProvider * @return void */ public function testValidTokenRequestDataVerifySource($method) { $middleware = new CsrfProtectionMiddleware(['verifyTokenSource' => true]); $token = $middleware->createToken(); $request = new ServerRequest([ 'environment' => [ 'REQUEST_METHOD' => $method, ], 'post' => ['_csrfToken' => $token], 'cookies' => ['csrfToken' => $token], ]); $response = new Response(); $closure = function ($request, $response) { $this->assertNull($request->getData('_csrfToken')); }; // No exception means everything is OK $middleware($request, $response, $closure); } /** * Test that request data works with the various http methods. * * @dataProvider httpMethodProvider * @return void */ public function testInvalidTokenRequestDataVerifySource($method) { $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class); $request = new ServerRequest([ 'environment' => [ 'REQUEST_METHOD' => $method, ], // Even though the tokens match they are not signed. 'post' => ['_csrfToken' => 'example-token'], 'cookies' => ['csrfToken' => 'example-token'], ]); $response = new Response(); $middleware = new CsrfProtectionMiddleware(['verifyTokenSource' => true]); $middleware($request, $response, $this->_getNextClosure()); } /** * Test that request data works with the various http methods. * * @dataProvider httpMethodProvider * @return void */ public function testInvalidTokenRequestData($method) { $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class); $request = new ServerRequest([ 'environment' => [ 'REQUEST_METHOD' => $method, ], 'post' => ['_csrfToken' => 'nope'], 'cookies' => ['csrfToken' => 'testing123'], ]); $response = new Response(); $middleware = new CsrfProtectionMiddleware(); $middleware($request, $response, $this->_getNextClosure()); } /** * Test that missing post field fails * * @return void */ public function testInvalidTokenRequestDataMissing() { $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class); $request = new ServerRequest([ 'environment' => [ 'REQUEST_METHOD' => 'POST', ], 'post' => [], 'cookies' => ['csrfToken' => 'testing123'], ]); $response = new Response(); $middleware = new CsrfProtectionMiddleware(); $middleware($request, $response, $this->_getNextClosure()); } /** * Test that request data works with the various http methods. * * @return void */ public function testInvalidTokenNonStringData() { $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class); $request = new ServerRequest([ 'environment' => [ 'REQUEST_METHOD' => 'POST', ], 'post' => ['_csrfToken' => ['nope']], 'cookies' => ['csrfToken' => ['nope']], ]); $response = new Response(); $middleware = new CsrfProtectionMiddleware(); $middleware($request, $response, $this->_getNextClosure()); } /** * Test that missing header and cookie fails * * @dataProvider httpMethodProvider * @return void */ public function testInvalidTokenMissingCookie($method) { $this->expectException(\Cake\Http\Exception\InvalidCsrfTokenException::class); $request = new ServerRequest([ 'environment' => [ 'REQUEST_METHOD' => $method, ], 'post' => ['_csrfToken' => 'could-be-valid'], 'cookies' => [], ]); $response = new Response(); $middleware = new CsrfProtectionMiddleware(); $middleware($request, $response, $this->_getNextClosure()); } /** * Test that the configuration options work. * * @return void */ public function testConfigurationCookieCreate() { $request = new ServerRequest([ 'environment' => ['REQUEST_METHOD' => 'GET'], 'webroot' => '/dir/', ]); $response = new Response(); $closure = function ($request, $response) { $this->assertEmpty($response->getCookie('csrfToken')); $cookie = $response->getCookie('token'); $this->assertNotEmpty($cookie, 'Should set a token.'); $this->assertRegExp('/^[a-f0-9]+$/', $cookie['value'], 'Should look like a hash.'); $this->assertWithinRange((new Time('+1 hour'))->format('U'), $cookie['expire'], 1, 'session duration.'); $this->assertEquals('/dir/', $cookie['path'], 'session path.'); $this->assertTrue($cookie['secure'], 'cookie security flag missing'); $this->assertTrue($cookie['httpOnly'], 'cookie httpOnly flag missing'); }; $middleware = new CsrfProtectionMiddleware([ 'cookieName' => 'token', 'expiry' => '+1 hour', 'secure' => true, 'httpOnly' => true, ]); $middleware($request, $response, $closure); } /** * Test that the configuration options work. * * There should be no exception thrown. * * @return void */ public function testConfigurationValidate() { $request = new ServerRequest([ 'environment' => ['REQUEST_METHOD' => 'POST'], 'cookies' => ['csrfToken' => 'nope', 'token' => 'yes'], 'post' => ['_csrfToken' => 'no match', 'token' => 'yes'], ]); $response = new Response(); $middleware = new CsrfProtectionMiddleware([ 'cookieName' => 'token', 'field' => 'token', 'expiry' => 90, ]); $response = $middleware($request, $response, $this->_getNextClosure()); $this->assertInstanceOf(Response::class, $response); } /** * @return void */ public function testSkippingTokenCheckUsingWhitelistCallback() { $request = new ServerRequest([ 'environment' => [ 'REQUEST_METHOD' => 'POST', ], ]); $response = new Response(); $middleware = new CsrfProtectionMiddleware(); $middleware->whitelistCallback(function (ServerRequestInterface $request) { $this->assertSame('POST', $request->getServerParams()['REQUEST_METHOD']); return true; }); $response = $middleware($request, $response, $this->_getNextClosure()); $this->assertInstanceOf(Response::class, $response); } /** * Test the situation where the app is upgraded from 3.9.2 or earlier to 3.9.3 or later, * without deleting route cache. * * @return void */ public function testMissingSamesite() { $request = new ServerRequest([ 'environment' => ['REQUEST_METHOD' => 'GET'], 'webroot' => '/dir/', ]); $response = new Response(); $closure = function ($request, $response) { }; $middleware = new CsrfProtectionMiddleware(); // simulate 3.9.2 or earlier by deleting "samesite" config $reflection = new \ReflectionClass($middleware); $property = $reflection->getProperty('_config'); $property->setAccessible(true); $defaultConfig = $property->getValue($middleware); unset($defaultConfig['samesite']); $property->setValue($middleware, $defaultConfig); $middleware($request, $response, $closure); $this->assertTrue(true); } }