Skip to content

Commit 695bd1a

Browse files
feature #54443 [Security] Add support for dynamic CSRF id with Expression in #[IsCsrfTokenValid] (yguedidi)
This PR was merged into the 7.1 branch. Discussion ---------- [Security] Add support for dynamic CSRF id with Expression in `#[IsCsrfTokenValid]` | Q | A | ------------- | --- | Branch? | 7.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | continuation of #52961 from Hackday | License | MIT Use case is for example on a list page with delete action per item, and you want a CSRF token per item, so in the template you have something like the following: ```twig {# in a loop over multiple posts #} <form action="{{ path('post_delete', {post: post.id}) }}" method="POST"> <input type="hidden" name="_token" value="{{ csrf_token('delete-post-' ~ post.id) }}"> ... </form> ``` The new feature will allow: ```php #[IsCsrfTokenValid(new Expression('"delete-post-" ~ args["post"].id'))] public function delete(Request $request, Post $post): Response { // ... delete the post } ``` Maybe this need more tests but need help identify which test cases are useful. Hope this can pass before the feature freeze Commits ------- 8f99ca5 Add support for dynamic CSRF id in IsCsrfTokenValid
2 parents 4c1d8eb + 8f99ca5 commit 695bd1a

File tree

7 files changed

+85
-4
lines changed

7 files changed

+85
-4
lines changed

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfFeaturesPass.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1515
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\ContainerInterface;
1617
use Symfony\Component\DependencyInjection\Reference;
1718
use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface;
1819
use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener;
@@ -35,6 +36,10 @@ public function process(ContainerBuilder $container): void
3536

3637
private function registerCsrfProtectionListener(ContainerBuilder $container): void
3738
{
39+
if (!$container->hasDefinition('cache.system')) {
40+
$container->removeDefinition('cache.security_is_csrf_token_valid_attribute_expression_language');
41+
}
42+
3843
if (!$container->has('security.authenticator.manager') || !$container->has('security.csrf.token_manager')) {
3944
return;
4045
}
@@ -45,6 +50,7 @@ private function registerCsrfProtectionListener(ContainerBuilder $container): vo
4550

4651
$container->register('controller.is_csrf_token_valid_attribute_listener', IsCsrfTokenValidAttributeListener::class)
4752
->addArgument(new Reference('security.csrf.token_manager'))
53+
->addArgument(new Reference('security.is_csrf_token_valid_attribute_expression_language', ContainerInterface::NULL_ON_INVALID_REFERENCE))
4854
->addTag('kernel.event_subscriber');
4955
}
5056

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ public function load(array $configs, ContainerBuilder $container): void
123123
$container->removeDefinition('security.expression_language');
124124
$container->removeDefinition('security.access.expression_voter');
125125
$container->removeDefinition('security.is_granted_attribute_expression_language');
126+
$container->removeDefinition('security.is_csrf_token_valid_attribute_expression_language');
126127
}
127128

128129
if (!class_exists(PasswordHasherExtension::class)) {

src/Symfony/Bundle/SecurityBundle/Resources/config/security.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,5 +303,12 @@
303303
->set('cache.security_is_granted_attribute_expression_language')
304304
->parent('cache.system')
305305
->tag('cache.pool')
306+
307+
->set('security.is_csrf_token_valid_attribute_expression_language', BaseExpressionLanguage::class)
308+
->args([service('cache.security_is_csrf_token_valid_attribute_expression_language')->nullOnInvalid()])
309+
310+
->set('cache.security_is_csrf_token_valid_attribute_expression_language')
311+
->parent('cache.system')
312+
->tag('cache.pool')
306313
;
307314
};

src/Symfony/Component/Security/Http/Attribute/IsCsrfTokenValid.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@
1111

1212
namespace Symfony\Component\Security\Http\Attribute;
1313

14+
use Symfony\Component\ExpressionLanguage\Expression;
15+
1416
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
1517
final class IsCsrfTokenValid
1618
{
1719
public function __construct(
1820
/**
19-
* Sets the id used when generating the token.
21+
* Sets the id, or an Expression evaluated to the id, used when generating the token.
2022
*/
21-
public string $id,
23+
public string|Expression $id,
2224

2325
/**
2426
* Sets the key of the request that contains the actual token value that should be validated.

src/Symfony/Component/Security/Http/EventListener/IsCsrfTokenValidAttributeListener.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
namespace Symfony\Component\Security\Http\EventListener;
1313

1414
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\ExpressionLanguage\Expression;
16+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
17+
use Symfony\Component\HttpFoundation\Request;
1518
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
1619
use Symfony\Component\HttpKernel\KernelEvents;
1720
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
@@ -26,6 +29,7 @@ final class IsCsrfTokenValidAttributeListener implements EventSubscriberInterfac
2629
{
2730
public function __construct(
2831
private readonly CsrfTokenManagerInterface $csrfTokenManager,
32+
private ?ExpressionLanguage $expressionLanguage = null,
2933
) {
3034
}
3135

@@ -37,9 +41,12 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo
3741
}
3842

3943
$request = $event->getRequest();
44+
$arguments = $event->getNamedArguments();
4045

4146
foreach ($attributes as $attribute) {
42-
if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($attribute->id, $request->request->getString($attribute->tokenKey)))) {
47+
$id = $this->getTokenId($attribute->id, $request, $arguments);
48+
49+
if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($id, $request->request->getString($attribute->tokenKey)))) {
4350
throw new InvalidCsrfTokenException('Invalid CSRF token.');
4451
}
4552
}
@@ -49,4 +56,18 @@ public static function getSubscribedEvents(): array
4956
{
5057
return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 25]];
5158
}
59+
60+
private function getTokenId(string|Expression $id, Request $request, array $arguments): string
61+
{
62+
if (!$id instanceof Expression) {
63+
return $id;
64+
}
65+
66+
$this->expressionLanguage ??= new ExpressionLanguage();
67+
68+
return (string) $this->expressionLanguage->evaluate($id, [
69+
'request' => $request,
70+
'args' => $arguments,
71+
]);
72+
}
5273
}

src/Symfony/Component/Security/Http/Tests/EventListener/IsCsrfTokenValidAttributeListenerTest.php

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace EventListener;
12+
namespace Symfony\Component\Security\Http\Tests\EventListener;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\ExpressionLanguage\Expression;
16+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
1517
use Symfony\Component\HttpFoundation\Request;
1618
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
1719
use Symfony\Component\HttpKernel\HttpKernelInterface;
@@ -86,6 +88,37 @@ public function testIsCsrfTokenValidCalledCorrectly()
8688
$listener->onKernelControllerArguments($event);
8789
}
8890

91+
public function testIsCsrfTokenValidCalledCorrectlyWithCustomExpressionId()
92+
{
93+
$request = new Request(query: ['id' => '123'], request: ['_token' => 'bar']);
94+
95+
$csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class);
96+
$csrfTokenManager->expects($this->once())
97+
->method('isTokenValid')
98+
->with(new CsrfToken('foo_123', 'bar'))
99+
->willReturn(true);
100+
101+
$expressionLanguage = $this->createMock(ExpressionLanguage::class);
102+
$expressionLanguage->expects($this->once())
103+
->method('evaluate')
104+
->with(new Expression('"foo_" ~ args.id'), [
105+
'args' => ['id' => '123'],
106+
'request' => $request,
107+
])
108+
->willReturn('foo_123');
109+
110+
$event = new ControllerArgumentsEvent(
111+
$this->createMock(HttpKernelInterface::class),
112+
[new IsCsrfTokenValidAttributeMethodsController(), 'withCustomExpressionId'],
113+
['123'],
114+
$request,
115+
null
116+
);
117+
118+
$listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager, $expressionLanguage);
119+
$listener->onKernelControllerArguments($event);
120+
}
121+
89122
public function testIsCsrfTokenValidCalledCorrectlyWithCustomTokenKey()
90123
{
91124
$request = new Request(request: ['my_token_key' => 'bar']);

src/Symfony/Component/Security/Http/Tests/Fixtures/IsCsrfTokenValidAttributeMethodsController.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Security\Http\Tests\Fixtures;
1313

14+
use Symfony\Component\ExpressionLanguage\Expression;
1415
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
1516

1617
class IsCsrfTokenValidAttributeMethodsController
@@ -24,6 +25,16 @@ public function withDefaultTokenKey()
2425
{
2526
}
2627

28+
#[IsCsrfTokenValid(new Expression('"foo_" ~ args.id'))]
29+
public function withCustomExpressionId(string $id)
30+
{
31+
}
32+
33+
#[IsCsrfTokenValid(new Expression('"foo_" ~ args.slug'))]
34+
public function withInvalidExpressionId(string $id)
35+
{
36+
}
37+
2738
#[IsCsrfTokenValid('foo', tokenKey: 'my_token_key')]
2839
public function withCustomTokenKey()
2940
{

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy