8000 feat(HttpKernel): add `#[IsSignatureValid]` attribute with exception-… · symfony/symfony@5881d55 · GitHub
[go: up one dir, main page]

Skip to content
8000

Commit 5881d55

Browse files
committed
feat(HttpKernel): add #[IsSignatureValid] attribute with exception-based handling
1 parent 53b6121 commit 5881d55

File tree

8 files changed

+519
-0
lines changed

8 files changed

+519
-0
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
use Symfony\Component\Security\Core\User\ChainUserProvider;
6060
use Symfony\Component\Security\Core\User\UserCheckerInterface;
6161
use Symfony\Component\Security\Core\User\UserProviderInterface;
62+
use Symfony\Component\Security\Http\Attribute\IsSignatureValid;
6263
use Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel;
6364
use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticator;
6465
use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener;
@@ -133,6 +134,9 @@ public function load(array $configs, ContainerBuilder $container): void
133134
$container->removeDefinition('form.type_extension.form.password_hasher');
134135
$container->removeDefinition('form.type_extension.password.password_hasher');
135136
}
137+
if (!class_exists(IsSignatureValid::class)) {
138+
$container->removeDefinition('controller.is_signature_valid_attribute_listener');
139+
}
136140

137141
// set some global scalars
138142
$container->setParameter('security.access.denied_url', $config['access_denied_url']);

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
use Symfony\Component\Security\Http\Controller\SecurityTokenValueResolver;
4949
use Symfony\Component\Security\Http\Controller\UserValueResolver;
5050
use Symfony\Component\Security\Http\EventListener\IsGrantedAttributeListener;
51+
use Symfony\Component\Security\Http\EventListener\IsSignatureValidAttributeListener;
5152
use Symfony\Component\Security\Http\Firewall;
5253
use Symfony\Component\Security\Http\FirewallMapInterface;
5354
use Symfony\Component\Security\Http\HttpUtils;
@@ -323,5 +324,11 @@
323324
->set('cache.security_is_csrf_token_valid_attribute_expression_language')
324325
->parent('cache.system')
325326
->tag('cache.pool')
327+
328+
->set('controller.is_signature_valid_attribute_listener', IsSignatureValidAttributeListener::class)
329+
->args([
330+
service('uri_signer'),
331+
])
332+
->tag('kernel.event_subscriber')
326333
;
327334
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\Attribute;
13+
14+
use Symfony\Component\HttpFoundation\Response;
15+
use Symfony\Component\HttpFoundation\UriSigner;
16+
17+
/**
18+
* Attribute to ensure the request URI contains a valid signature before allowing controller execution.
19+
*
20+
* When applied, this attribute verifies that the request is signed and the signature is still valid (e.g., not expired).
21+
* Behavior can be customized to either return a specific HTTP status code or throw an exception to be handled globally.
22+
*
23+
* @author Santiago San Martin <sanmartindev@gmail.com>
24+
*/
25+
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
26+
final class IsSignatureValid
27+
{
28+
/**
29+
* @param int|null $validationFailedStatusCode The HTTP status code to return if the signature is invalid
30+
* @param bool|null $throw If true, a {@see \Symfony\Component\HttpFoundation\Exception\SignedUriException} is thrown on signature failure instead of returning a response
31+
* @param array|string $methods HTTP methods that require signature validation
32+
*/
33+
public function __construct(
34+
public ?int $validationFailedStatusCode = Response::HTTP_NOT_FOUND,
35+
public ?bool $throw = null,
36+
public array|string $methods = [],
37+
) {
38+
if ($this->throw && !method_exists(UriSigner::class, 'verify')) {
39+
throw new \LogicException('The method UriSigner::verify is required. Please upgrade symfony/http-foundation to 7.4 version or ensure it is correctly installed.');
40+
}
41+
}
42+
}

src/Symfony/Component/Security/Http/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.4
5+
---
6+
7+
* Add `#[IsSignatureValid]` attribute to validate URI signatures
8+
49
7.3
510
---
611

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\HttpFoundation\UriSigner;
16+
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
17+
use Symfony\Component\HttpKernel\Exception\HttpException;
18+
use Symfony\Component\HttpKernel\KernelEvents;
19+
use Symfony\Component\Security\Http\Attribute\IsSignatureValid;
20+
21+
/**
22+
* Handles the IsSignatureValid attribute.
23+
*
24+
* @author Santiago San Martin <sanmartindev@gmail.com>
25+
*/
26+
class IsSignatureValidAttributeListener implements EventSubscriberInterface
27+
{
28+
public function __construct(
29+
private readonly UriSigner $uriSigner,
30+
) {
31+
}
32+
33+
public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
34+
{
35+
/** @var IsSignatureValid[] $attributes */
36+
if (!\is_array($attributes = $event->getAttributes()[IsSignatureValid::class] ?? null)) {
37+
return;
38+
}
39+
40+
$request = $event->getRequest();
41+
foreach ($attributes as $attribute) {
42+
$methods = \array_map('strtoupper', (array) $attribute->methods);
43+
if ($methods && !\in_array($request->getMethod(), $methods, true)) {
44+
continue;
45+
}
46+
47+
if ($attribute->throw) {
48+
$this->uriSigner->verify($request);
49+
continue;
50+
}
51+
if (!$this->uriSigner->checkRequest($request)) {
52+
throw new HttpException($attribute->validationFailedStatusCode, 'The URI signature is invalid.');
53+
}
54+
}
55+
}
56+
57+
public static function getSubscribedEvents(): array
58+
{
59+
return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 30]];
60+
}
61+
}

0 commit comments

Comments
 (0)
0