8000 Add support for dynamic CSRF id in IsCsrfTokenValid · symfony/symfony@e40a7e1 · GitHub
[go: up one dir, main page]

Skip to content

Commit e40a7e1

Browse files
committed
Add support for dynamic CSRF id in IsCsrfTokenValid
1 parent 3f2ed0f commit e40a7e1

File tree

6 files changed

+105
-3
lines changed

6 files changed

+105
-3
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111

1212
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler;
1313

14+
use Symfony\Component\DependencyInjection\ChildDefinition;
1415
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1516
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\ContainerInterface;
1618
use Symfony\Component\DependencyInjection\Reference;
19+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
1720
use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface;
1821
use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener;
1922
use Symfony\Component\Security\Http\EventListener\CsrfTokenClearingLogoutListener;
@@ -39,12 +42,22 @@ private function registerCsrfProtectionListener(ContainerBuilder $container): vo
3942
return;
4043
}
4144

45+
if ($container->has('cache.system')) {
46+
$cache = new ChildDefinition('cache.system');
47+
$cache->addTag('cache.pool');
48+
$container->addDefinitions(['cache.security_is_csrf_token_valid_attribute_expression_language' => $cache]);
49+
}
50+
51+
$container->register('security.is_csrf_token_valid_attribute_expression_language', ExpressionLanguage::class)
52+
->addArgument(new Reference('cache.security_is_csrf_token_valid_attribute_expression_language', ContainerInterface::NULL_ON_INVALID_REFERENCE));
53+
4254
$container->register('security.listener.csrf_protection', CsrfProtectionListener::class)
4355
->addArgument(new Reference('security.csrf.token_manager'))
4456
->addTag('kernel.event_subscriber');
4557

4658
$container->register('controller.is_csrf_token_valid_attribute_listener', IsCsrfTokenValidAttributeListener::class)
4759
->addArgument(new Reference('security.csrf.token_manager'))
60+
->addArgument(new Reference('security.is_csrf_token_valid_attribute_expression_language', ContainerInterface::NULL_ON_INVALID_REFERENCE))
4861
->addTag('kernel.event_subscriber');
4962
}
5063

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

Lines changed: 3 additions & 1 deletion
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
/**
1921
* Sets 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/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add `#[IsCsrfTokenValid]` attribute
88
* Add CAS 2.0 access token handler
99
* Make empty username or empty password on form login attempts return Bad Request (400)
10+
* Add support for dynamic CSRF id in `#[IsCsrfTokenValid]`
1011

1112
7.0
1213
---

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->getIsCsrfTokenValidId($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 getIsCsrfTokenValidId(string|Expression $id, Request $request, array $arguments): mixed
61+
{
62+
if ($id instanceof Expression) {
63+
$this->expressionLanguage ??= new ExpressionLanguage();
64+
65+
return $this->expressionLanguage->evaluate($id, [
66+
'request' => $request,
67+
'args' => $arguments,
68+
]);
69+
}
70+
71+
return $id;
72+
}
5273
}

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

Lines changed: 55 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']);
@@ -130,6 +163,27 @@ public function testIsCsrfTokenValidCalledCorrectlyWithInvalidTokenKey()
130163
$listener->onKernelControllerArguments($event);
131164
}
132165

166+
public function testExceptionWhenInvalidExpressionId()
167+
{
168+
$this->expectException(\RuntimeException::class);
169+
170+
$request = new Request(query: ['id' => '123'], request: ['_token' => 'bar']);
171+
172+
$csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class);
173+
174+
$event = new ControllerArgumentsEvent(
175+
$this->createMock(HttpKernelInterface::class),
176+
[new< 520 /span> IsCsrfTokenValidAttributeMethodsController(), 'withInvalidExpressionId'],
177+
[],
178+
$request,
179+
null
180+
);
181+
182+
$listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager);
183+
184+
$listener->onKernelControllerArguments($event);
185+
}
186+
133187
public function testExceptionWhenInvalidToken()
134188
{
135189
$this->expectException(InvalidCsrfTokenException::class);

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)
0