From 7e15feefcb486750923fc68dbdb8a3a92a4eb2fd Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Fri, 8 Nov 2024 18:29:10 +0700 Subject: [PATCH] rename CsrfMiddleware to CsrfTokenMiddleware --- README.md | 34 +++++++++---------- src/CsrfHeaderMiddleware.php | 5 +++ src/CsrfMiddleware.php | 5 +++ src/CsrfTokenMiddleware.php | 15 +++++++- ...php => CsrfTokenMiddlewareProcessTest.php} | 32 ++++++++--------- ...reTest.php => CsrfTokenMiddlewareTest.php} | 2 +- ...acTokenCsrfTokenMiddlewareProcessTest.php} | 4 +-- ...erTokenCsrfTokenMiddlewareProcessTest.php} | 6 ++-- 8 files changed, 63 insertions(+), 40 deletions(-) rename tests/{TokenCsrfMiddlewareTest.php => CsrfTokenMiddlewareProcessTest.php} (89%) rename tests/{CsrfMiddlewareTest.php => CsrfTokenMiddlewareTest.php} (97%) rename tests/Hmac/{HmacTokenCsrfMiddlewareTest.php => HmacTokenCsrfTokenMiddlewareProcessTest.php} (76%) rename tests/Synchronizer/{SynchronizerTokenCsrfMiddlewareTest.php => SynchronizerTokenCsrfTokenMiddlewareProcessTest.php} (86%) diff --git a/README.md b/README.md index 9c4f091..5754021 100644 --- a/README.md +++ b/README.md @@ -103,30 +103,30 @@ $failureHandler = new class ($responseFactory) implements RequestHandlerInterfac $middleware = new CsrfTokenMiddleware($responseFactory, $csrfToken, $failureHandler); ``` -By default, `CsrfMiddleware` considers `GET`, `HEAD`, `OPTIONS` methods as safe operations and doesn't perform CSRF validation. You can change this behavior as follows: +By default, `CsrfTokenMiddleware` considers `GET`, `HEAD`, `OPTIONS` methods as safe operations and doesn't perform CSRF validation. You can change this behavior as follows: ```php -use Yiisoft\Csrf\CsrfMiddleware; +use Yiisoft\Csrf\CsrfTokenMiddleware; use Yiisoft\Http\Method; -$csrfMiddleware = $container->get(CsrfMiddleware::class); +$csrfTokenMiddleware = $container->get(CsrfTokenMiddleware::class); // Returns a new instance with the specified list of safe methods. -$csrfMiddleware = $csrfMiddleware->withSafeMethods([Method::OPTIONS]); +$csrfTokenMiddleware = $csrfTokenMiddleware->withSafeMethods([Method::OPTIONS]); // Returns a new instance with the specified header name. -$csrfMiddleware = $csrfMiddleware->withHeaderName('X-CSRF-PROTECTION'); +$csrfTokenMiddleware = $csrfTokenMiddleware->withHeaderName('X-CSRF-PROTECTION'); ``` -or define the `CsrfMiddleware` configuration in the DI container: +or define the `CsrfTokenMiddleware` configuration in the DI container: ```php // [yiisoft/di](https://github.com/yiisoft/di) configuration file example -use Yiisoft\Csrf\CsrfMiddleware; +use Yiisoft\Csrf\CsrfTokenMiddleware; use Yiisoft\Http\Method; return [ - CsrfMiddleware::class => [ + CsrfTokenMiddleware::class => [ 'withSafeMethods()' => [[Method::OPTIONS]], 'withHeaderName()' => ['X-CSRF-PROTECTION'], ], @@ -273,7 +273,7 @@ return [ The use of a custom request header for CSRF protection is based on the CORS Protocol. Thus, you **must** configure the CORS module to allow or deny cross-origin access to the backend API. -> **Warning** +>**Warning** >`CsrfHeaderMiddleware` can be used to prevent forgery of same-origin requests and requests from the list of specific origins only. @@ -403,24 +403,24 @@ Access-Control-Allow-Origin: $frontendOrigin #### Configure middlewares stack -By default, `CsrfMiddleware` considers `GET`, `HEAD`, `OPTIONS` methods as safe operations and doesn't perform CSRF validation. +By default, `CsrfTokenMiddleware` considers `GET`, `HEAD`, `OPTIONS` methods as safe operations and doesn't perform CSRF validation. In JavaScript-based apps, requests are made programmatically; therefore, to increase application protection, the only `OPTIONS` method can be considered safe and need not be appended with a CSRF token header. -Configure `CsrfMiddleware` safe methods: +Configure `CsrfTokenMiddleware` safe methods: ```php -$csrfMiddleware = $container->get(CsrfMiddleware::class); -$csrfMiddleware = $csrfMiddleware->withSafeMethods([Method::OPTIONS]); +$csrfTokenMiddleware = $container->get(CsrfTokenMiddleware::class); +$csrfTokenMiddleware = $csrfTokenMiddleware->withSafeMethods([Method::OPTIONS]); ``` -Add `CsrfMiddleware` to the main middleware stack: +Add `CsrfTokenMiddleware` to the main middleware stack: ```php $middlewareDispatcher = $injector->make(MiddlewareDispatcher::class); $middlewareDispatcher = $middlewareDispatcher->withMiddlewares([ ErrorCatcher::class, SessionMiddleware::class, - CsrfMiddleware::class, // <-- add this + CsrfTokenMiddleware::class, // <-- add this Router::class, ]); ``` @@ -431,7 +431,7 @@ or to the routes that must be protected: $collector = $container->get(RouteCollectorInterface::class); $collector->addGroup( Group::create('/api') - ->middleware(CsrfMiddleware::class) // <-- add this + ->middleware(CsrfTokenMiddleware::class) // <-- add this ->routes($routes) ); ``` @@ -476,7 +476,7 @@ let csrfToken = await response.text(); let csrfToken = response.headers.get('X-CSRF-TOKEN'); ``` -Add to all requests a custom header defined in the `CsrfMiddleware` with acquired CSRF-token value. +Add to all requests a custom header defined in the `CsrfTokenMiddleware` with acquired CSRF-token value. ```js let response = fetch('https://api.example.com/whoami', { diff --git a/src/CsrfHeaderMiddleware.php b/src/CsrfHeaderMiddleware.php index 5daf920..6c7ca11 100644 --- a/src/CsrfHeaderMiddleware.php +++ b/src/CsrfHeaderMiddleware.php @@ -25,6 +25,11 @@ final class CsrfHeaderMiddleware implements MiddlewareInterface public const HEADER_NAME = 'X-CSRF-Header'; private string $headerName = self::HEADER_NAME; + + /** + * @var array "unsafe" methods not triggered a CORS-preflight request + * @link https://fetch.spec.whatwg.org/#http-cors-protocol + */ private array $unsafeMethods = [Method::GET, Method::HEAD, Method::POST]; private ResponseFactoryInterface $responseFactory; diff --git a/src/CsrfMiddleware.php b/src/CsrfMiddleware.php index bd9b292..79306e6 100644 --- a/src/CsrfMiddleware.php +++ b/src/CsrfMiddleware.php @@ -28,6 +28,11 @@ final class CsrfMiddleware implements MiddlewareInterface private string $parameterName = self::PARAMETER_NAME; private string $headerName = self::HEADER_NAME; + + /** + * @var array "safe" methods skipped on CSRF token validation + * @link https://datatracker.ietf.org/doc/html/rfc9110#name-safe-methods + */ private array $safeMethods = [Method::GET, Method::HEAD, Method::OPTIONS]; private ResponseFactoryInterface $responseFactory; diff --git a/src/CsrfTokenMiddleware.php b/src/CsrfTokenMiddleware.php index 0ef2b3b..0185ef3 100644 --- a/src/CsrfTokenMiddleware.php +++ b/src/CsrfTokenMiddleware.php @@ -28,6 +28,12 @@ final class CsrfTokenMiddleware implements MiddlewareInterface private string $parameterName = self::PARAMETER_NAME; private string $headerName = self::HEADER_NAME; + /** + * @var array "safe" methods skipped on CSRF token validation + * @link https://datatracker.ietf.org/doc/html/rfc9110#name-safe-methods + */ + private array $safeMethods = [Method::GET, Method::HEAD, Method::OPTIONS]; + private ResponseFactoryInterface $responseFactory; private CsrfTokenInterface $token; private ?RequestHandlerInterface $failureHandler; @@ -73,6 +79,13 @@ public function withHeaderName(string $name): self return $new; } + public function withSafeMethods(array $methods): self + { + $new = clone $this; + $new->safeMethods = $methods; + return $new; + } + public function getParameterName(): string { return $this->parameterName; @@ -85,7 +98,7 @@ public function getHeaderName(): string private function validateCsrfToken(ServerRequestInterface $request): bool { - if (in_array($request->getMethod(), [Method::GET, Method::HEAD, Method::OPTIONS], true)) { + if (in_array($request->getMethod(), $this->safeMethods, true)) { return true; } diff --git a/tests/TokenCsrfMiddlewareTest.php b/tests/CsrfTokenMiddlewareProcessTest.php similarity index 89% rename from tests/TokenCsrfMiddlewareTest.php rename to tests/CsrfTokenMiddlewareProcessTest.php index 77cbb06..66c8763 100644 --- a/tests/TokenCsrfMiddlewareTest.php +++ b/tests/CsrfTokenMiddlewareProcessTest.php @@ -18,7 +18,7 @@ use Yiisoft\Http\Status; use Yiisoft\Security\Random; -abstract class TokenCsrfMiddlewareTest extends TestCase +abstract class CsrfTokenMiddlewareProcessTest extends TestCase { private const PARAM_NAME = 'csrf'; @@ -26,28 +26,28 @@ abstract class TokenCsrfMiddlewareTest extends TestCase public function testGetIsAlwaysAllowed(): void { - $middleware = $this->createCsrfMiddleware(); + $middleware = $this->createCsrfTokenMiddleware(); $response = $middleware->process($this->createServerRequest(Method::GET), $this->createRequestHandler()); $this->assertEquals(200, $response->getStatusCode()); } public function testHeadIsAlwaysAllowed(): void { - $middleware = $this->createCsrfMiddleware(); + $middleware = $this->createCsrfTokenMiddleware(); $response = $middleware->process($this->createServerRequest(Method::HEAD), $this->createRequestHandler()); $this->assertEquals(200, $response->getStatusCode()); } public function testOptionsIsAlwaysAllowed(): void { - $middleware = $this->createCsrfMiddleware(); + $middleware = $this->createCsrfTokenMiddleware(); $response = $middleware->process($this->createServerRequest(Method::OPTIONS), $this->createRequestHandler()); $this->assertEquals(200, $response->getStatusCode()); } public function testValidTokenInBodyPostRequestResultIn200(): void { - $middleware = $this->createCsrfMiddleware(); + $middleware = $this->createCsrfTokenMiddleware(); $response = $middleware->process( $this->createPostServerRequestWithBodyToken($this->token), $this->createRequestHandler() @@ -57,7 +57,7 @@ public function testValidTokenInBodyPostRequestResultIn200(): void public function testValidTokenInBodyPutRequestResultIn200(): void { - $middleware = $this->createCsrfMiddleware(); + $middleware = $this->createCsrfTokenMiddleware(); $response = $middleware->process( $this->createPutServerRequestWithBodyToken($this->token), $this->createRequestHandler() @@ -67,7 +67,7 @@ public function testValidTokenInBodyPutRequestResultIn200(): void public function testValidTokenInBodyDeleteRequestResultIn200(): void { - $middleware = $this->createCsrfMiddleware(); + $middleware = $this->createCsrfTokenMiddleware(); $response = $middleware->process( $this->createDeleteServerRequestWithBodyToken($this->token), $this->createRequestHandler() @@ -77,7 +77,7 @@ public function testValidTokenInBodyDeleteRequestResultIn200(): void public function testValidTokenInHeaderResultIn200(): void { - $middleware = $this->createCsrfMiddleware(); + $middleware = $this->createCsrfTokenMiddleware(); $response = $middleware->process( $this->createPostServerRequestWithHeaderToken($this->token), $this->createRequestHandler() @@ -90,7 +90,7 @@ public function testValidTokenInCustomHeaderResultIn200(): void $headerName = 'CUSTOM-CSRF'; $middleware = $this - ->createCsrfMiddleware() + ->createCsrfTokenMiddleware() ->withHeaderName($headerName); $response = $middleware->process( $this->createPostServerRequestWithHeaderToken($this->token, $headerName), @@ -102,7 +102,7 @@ public function testValidTokenInCustomHeaderResultIn200(): void public function testInvalidTokenResultIn422(): void { - $middleware = $this->createCsrfMiddleware(); + $middleware = $this->createCsrfTokenMiddleware(); $response = $middleware->process( $this->createPostServerRequestWithBodyToken(Random::string()), @@ -126,7 +126,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface } }; - $middleware = $this->createCsrfMiddleware(null, $failureHandler); + $middleware = $this->createCsrfTokenMiddleware(null, $failureHandler); $response = $middleware->process( $this->createPostServerRequestWithBodyToken(Random::string()), @@ -139,14 +139,14 @@ public function handle(ServerRequestInterface $request): ResponseInterface public function testEmptyTokenInRequestResultIn422(): void { - $middleware = $this->createCsrfMiddleware(); + $middleware = $this->createCsrfTokenMiddleware(); $response = $middleware->process($this->createServerRequest(), $this->createRequestHandler()); $this->assertEquals(Status::UNPROCESSABLE_ENTITY, $response->getStatusCode()); } public function testUnsafeMethodPostRequestResultIn422(): void { - $middleware = $this->createCsrfMiddleware(); + $middleware = $this->createCsrfTokenMiddleware(); $response = $middleware->process( $this->createServerRequest(Method::POST), $this->createRequestHandler() @@ -158,7 +158,7 @@ public function testUnsafeMethodPostRequestResultIn422(): void public function testCustomSafeOptionsRequestResultIn200(): void { $middleware = $this - ->createCsrfMiddleware() + ->createCsrfTokenMiddleware() ->withSafeMethods([Method::OPTIONS]); $response = $middleware->process( $this->createServerRequest(Method::OPTIONS), @@ -170,7 +170,7 @@ public function testCustomSafeOptionsRequestResultIn200(): void public function testCustomUnsafeMethodGetRequestResultIn422(): void { $middleware = $this - ->createCsrfMiddleware() + ->createCsrfTokenMiddleware() ->withSafeMethods([Method::OPTIONS]); $response = $middleware->process( $this->createServerRequest(Method::GET), @@ -230,7 +230,7 @@ private function getBodyRequestParamsByToken(string $token): array ]; } - protected function createCsrfMiddleware( + protected function createCsrfTokenMiddleware( ?CsrfTokenInterface $csrfToken = null, RequestHandlerInterface $failureHandler = null ): CsrfTokenMiddleware { diff --git a/tests/CsrfMiddlewareTest.php b/tests/CsrfTokenMiddlewareTest.php similarity index 97% rename from tests/CsrfMiddlewareTest.php rename to tests/CsrfTokenMiddlewareTest.php index bb10e12..ec8efa3 100644 --- a/tests/CsrfMiddlewareTest.php +++ b/tests/CsrfTokenMiddlewareTest.php @@ -12,7 +12,7 @@ use Yiisoft\Csrf\Tests\Synchronizer\Storage\MockCsrfTokenStorage; use Yiisoft\Http\Method; -final class CsrfMiddlewareTest extends TestCase +final class CsrfTokenMiddlewareTest extends TestCase { public function testDefaultParameterName(): void { diff --git a/tests/Hmac/HmacTokenCsrfMiddlewareTest.php b/tests/Hmac/HmacTokenCsrfTokenMiddlewareProcessTest.php similarity index 76% rename from tests/Hmac/HmacTokenCsrfMiddlewareTest.php rename to tests/Hmac/HmacTokenCsrfTokenMiddlewareProcessTest.php index 6a6ab08..56e1537 100644 --- a/tests/Hmac/HmacTokenCsrfMiddlewareTest.php +++ b/tests/Hmac/HmacTokenCsrfTokenMiddlewareProcessTest.php @@ -7,10 +7,10 @@ use Yiisoft\Csrf\CsrfTokenInterface; use Yiisoft\Csrf\Hmac\HmacCsrfToken; use Yiisoft\Csrf\Tests\Hmac\IdentityGenerator\MockCsrfTokenIdentityGenerator; -use Yiisoft\Csrf\Tests\TokenCsrfMiddlewareTest; +use Yiisoft\Csrf\Tests\CsrfTokenMiddlewareProcessTest; use Yiisoft\Security\Random; -final class HmacTokenCsrfMiddlewareTest extends TokenCsrfMiddlewareTest +final class HmacTokenCsrfTokenMiddlewareProcessTest extends CsrfTokenMiddlewareProcessTest { protected function createCsrfToken(): CsrfTokenInterface { diff --git a/tests/Synchronizer/SynchronizerTokenCsrfMiddlewareTest.php b/tests/Synchronizer/SynchronizerTokenCsrfTokenMiddlewareProcessTest.php similarity index 86% rename from tests/Synchronizer/SynchronizerTokenCsrfMiddlewareTest.php rename to tests/Synchronizer/SynchronizerTokenCsrfTokenMiddlewareProcessTest.php index 958e590..c3a9783 100644 --- a/tests/Synchronizer/SynchronizerTokenCsrfMiddlewareTest.php +++ b/tests/Synchronizer/SynchronizerTokenCsrfTokenMiddlewareProcessTest.php @@ -9,14 +9,14 @@ use Yiisoft\Csrf\Synchronizer\Generator\RandomCsrfTokenGenerator; use Yiisoft\Csrf\Synchronizer\SynchronizerCsrfToken; use Yiisoft\Csrf\Tests\Synchronizer\Storage\MockCsrfTokenStorage; -use Yiisoft\Csrf\Tests\TokenCsrfMiddlewareTest; +use Yiisoft\Csrf\Tests\CsrfTokenMiddlewareProcessTest; use Yiisoft\Security\Random; -final class SynchronizerTokenCsrfMiddlewareTest extends TokenCsrfMiddlewareTest +final class SynchronizerTokenCsrfTokenMiddlewareProcessTest extends CsrfTokenMiddlewareProcessTest { public function testEmptyTokenInSessionResultIn422(): void { - $middleware = $this->createCsrfMiddleware( + $middleware = $this->createCsrfTokenMiddleware( new SynchronizerCsrfToken( new RandomCsrfTokenGenerator(), new MockCsrfTokenStorage()