diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 5ffc9f85f0c34..9dd35a826a06c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -16,6 +16,7 @@ CHANGELOG * Add `--method` option to the `debug:router` command * Auto-exclude DI extensions, test cases, entities and messenger messages * Add DI alias from `ServicesResetterInterface` to `services_resetter` + * Add `methods` argument in `#[IsCsrfTokenValid]` attribute 7.2 --- diff --git a/src/Symfony/Component/Security/Http/Attribute/IsCsrfTokenValid.php b/src/Symfony/Component/Security/Http/Attribute/IsCsrfTokenValid.php index ef598df2925fc..6226fb60bca5c 100644 --- a/src/Symfony/Component/Security/Http/Attribute/IsCsrfTokenValid.php +++ b/src/Symfony/Component/Security/Http/Attribute/IsCsrfTokenValid.php @@ -26,6 +26,12 @@ public function __construct( * Sets the key of the request that contains the actual token value that should be validated. */ public ?string $tokenKey = '_token', + + /** + * Sets the available http methods that can be used to validate the token. + * If not set, the token will be validated for all methods. + */ + public array|string $methods = [], ) { } } diff --git a/src/Symfony/Component/Security/Http/EventListener/IsCsrfTokenValidAttributeListener.php b/src/Symfony/Component/Security/Http/EventListener/IsCsrfTokenValidAttributeListener.php index 3e05c71dbbcd5..3075e3d07954b 100644 --- a/src/Symfony/Component/Security/Http/EventListener/IsCsrfTokenValidAttributeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/IsCsrfTokenValidAttributeListener.php @@ -45,6 +45,11 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo foreach ($attributes as $attribute) { $id = $this->getTokenId($attribute->id, $request, $arguments); + $methods = \array_map('strtoupper', (array) $attribute->methods); + + if ($methods && !\in_array($request->getMethod(), $methods, true)) { + continue; + } if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($id, $request->getPayload()->getString($attribute->tokenKey)))) { throw new InvalidCsrfTokenException('Invalid CSRF token.'); diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/IsCsrfTokenValidAttributeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/IsCsrfTokenValidAttributeListenerTest.php index 00d464a6c69da..6ce136ffc9a0b 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/IsCsrfTokenValidAttributeListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/IsCsrfTokenValidAttributeListenerTest.php @@ -206,4 +206,141 @@ public function testExceptionWhenInvalidToken() $listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager); $listener->onKernelControllerArguments($event); } + + public function testIsCsrfTokenValidCalledCorrectlyWithDeleteMethod() + { + $request = new Request(request: ['_token' => 'bar']); + $request->setMethod('DELETE'); + + $csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $csrfTokenManager->expects($this->once()) + ->method('isTokenValid') + ->with(new CsrfToken('foo', 'bar')) + ->willReturn(true); + + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + [new IsCsrfTokenValidAttributeMethodsController(), 'withDeleteMethod'], + [], + $request, + null + ); + + $listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager); + $listener->onKernelControllerArguments($event); + } + + public function testIsCsrfTokenValidIgnoredWithNonMatchingMethod() + { + $request = new Request(request: ['_token' => 'bar']); + $request->setMethod('POST'); + + $csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $csrfTokenManager->expects($this->never()) + ->method('isTokenValid') + ->with(new CsrfToken('foo', 'bar')); + + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + [new IsCsrfTokenValidAttributeMethodsController(), 'withDeleteMethod'], + [], + $request, + null + ); + + $listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager); + $listener->onKernelControllerArguments($event); + } + + public function testIsCsrfTokenValidCalledCorrectlyWithGetOrPostMethodWithGetMethod() + { + $request = new Request(request: ['_token' => 'bar']); + $request->setMethod('GET'); + + $csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $csrfTokenManager->expects($this->once()) + ->method('isTokenValid') + ->with(new CsrfToken('foo', 'bar')) + ->willReturn(true); + + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + [new IsCsrfTokenValidAttributeMethodsController(), 'withGetOrPostMethod'], + [], + $request, + null + ); + + $listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager); + $listener->onKernelControllerArguments($event); + } + + public function testIsCsrfTokenValidNoIgnoredWithGetOrPostMethodWithPutMethod() + { + $request = new Request(request: ['_token' => 'bar']); + $request->setMethod('PUT'); + + $csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $csrfTokenManager->expects($this->never()) + ->method('isTokenValid') + ->with(new CsrfToken('foo', 'bar')); + + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + [new IsCsrfTokenValidAttributeMethodsController(), 'withGetOrPostMethod'], + [], + $request, + null + ); + + $listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager); + $listener->onKernelControllerArguments($event); + } + + public function testIsCsrfTokenValidCalledCorrectlyWithInvalidTokenKeyAndPostMethod() + { + $this->expectException(InvalidCsrfTokenException::class); + + $request = new Request(request: ['_token' => 'bar']); + $request->setMethod('POST'); + + $csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $csrfTokenManager->expects($this->once()) + ->method('isTokenValid') + ->withAnyParameters() + ->willReturn(false); + + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + [new IsCsrfTokenValidAttributeMethodsController(), 'withPostMethodAndInvalidTokenKey'], + [], + $request, + null + ); + + $listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager); + $listener->onKernelControllerArguments($event); + } + + public function testIsCsrfTokenValidIgnoredWithInvalidTokenKeyAndUnavailableMethod() + { + $request = new Request(request: ['_token' => 'bar']); + $request->setMethod('PUT'); + + $csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $csrfTokenManager->expects($this->never()) + ->method('isTokenValid') + ->withAnyParameters(); + + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + [new IsCsrfTokenValidAttributeMethodsController(), 'withPostMethodAndInvalidTokenKey'], + [], + $request, + null + ); + + $listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager); + $listener->onKernelControllerArguments($event); + } } diff --git a/src/Symfony/Component/Security/Http/Tests/Fixtures/IsCsrfTokenValidAttributeMethodsController.php b/src/Symfony/Component/Security/Http/Tests/Fixtures/IsCsrfTokenValidAttributeMethodsController.php index baf45d77ac100..8555a43b4d42d 100644 --- a/src/Symfony/Component/Security/Http/Tests/Fixtures/IsCsrfTokenValidAttributeMethodsController.php +++ b/src/Symfony/Component/Security/Http/Tests/Fixtures/IsCsrfTokenValidAttributeMethodsController.php @@ -44,4 +44,19 @@ public function withCustomTokenKey() public function withInvalidTokenKey() { } + + #[IsCsrfTokenValid('foo', methods: 'DELETE')] + public function withDeleteMethod() + { + } + + #[IsCsrfTokenValid('foo', methods: ['GET', 'POST'])] + public function withGetOrPostMethod() + { + } + + #[IsCsrfTokenValid('foo', tokenKey: 'invalid_token_key', methods: ['POST'])] + public function withPostMethodAndInvalidTokenKey() + { + } }