Skip to content

Commit c304021

Browse files
committed
[HttpKernel] Add #[IsSignatureValid] attribute
1 parent 73ce8e5 commit c304021

File tree

14 files changed

+473
-0
lines changed

14 files changed

+473
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Allow using their name without added suffix when using `#[Target]` for custom services
99
* Deprecate `Symfony\Bundle\FrameworkBundle\Console\Application::add()` in favor of `Symfony\Bundle\FrameworkBundle\Console\Application::addCommand()`
1010
* Add `assertEmailAddressNotContains()` to the `MailerAssertionsTrait`
11+
* Add autoconfiguration tag `kernel.uri_signer` to `Symfony\Component\HttpFoundation\UriSigner`
1112

1213
7.3
1314
---

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class UnusedTagsPass implements CompilerPassInterface
6161
'kernel.fragment_renderer',
6262
'kernel.locale_aware',
6363
'kernel.reset',
64+
'kernel.uri_signer',
6465
'ldap',
6566
'mailer.transport_factory',
6667
'messenger.bus',

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,15 @@
9999
use Symfony\Component\HttpClient\ThrottlingHttpClient;
100100
use Symfony\Component\HttpClient\UriTemplateHttpClient;
101101
use Symfony\Component\HttpFoundation\Request;
102+
use Symfony\Component\HttpFoundation\UriSigner;
102103
use Symfony\Component\HttpKernel\Attribute\AsController;
103104
use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver;
104105
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
105106
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
106107
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
107108
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
108109
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
110+
use Symfony\Component\HttpKernel\EventListener\IsSignatureValidAttributeListener;
109111
use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator;
110112
use Symfony\Component\HttpKernel\Profiler\ProfilerStateChecker;
111113
use Symfony\Component\JsonStreamer\Attribute\JsonStreamable;
@@ -279,6 +281,9 @@ public function load(array $configs, ContainerBuilder $container): void
279281
if (!class_exists(RunProcessMessageHandler::class)) {
280282
$container->removeDefinition('process.messenger.process_message_handler');
281283
}
284+
if (!class_exists(IsSignatureValidAttributeListener::class)) {
285+
$container->removeDefinition('controller.is_signature_valid_attribute_listener');
286+
}
282287

283288
if ($this->hasConsole()) {
284289
$loader->load('console.php');
@@ -753,6 +758,8 @@ public function load(array $configs, ContainerBuilder $container): void
753758
->addTag('mime.mime_type_guesser');
754759
$container->registerForAutoconfiguration(LoggerAwareInterface::class)
755760
->addMethodCall('setLogger', [new Reference('logger')]);
761+
$container->registerForAutoconfiguration(UriSigner::class)
762+
->addTag('kernel.uri_signer');
756763

757764
$container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute, \ReflectionClass|\ReflectionMethod $reflector) {
758765
$tagAttributes = get_object_vars($attribute);

src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Symfony\Component\HttpKernel\EventListener\CacheAttributeListener;
3333
use Symfony\Component\HttpKernel\EventListener\DisallowRobotsIndexingListener;
3434
use Symfony\Component\HttpKernel\EventListener\ErrorListener;
35+
use Symfony\Component\HttpKernel\EventListener\IsSignatureValidAttributeListener;
3536
use Symfony\Component\HttpKernel\EventListener\LocaleListener;
3637
use Symfony\Component\HttpKernel\EventListener\ResponseListener;
3738
use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener;
@@ -148,6 +149,13 @@
148149
->tag('kernel.event_subscriber')
149150
->tag('kernel.reset', ['method' => '?reset'])
150151

152+
->set('controller.is_signature_valid_attribute_listener', IsSignatureValidAttributeListener::class)
153+
->args([
154+
service('uri_signer'),
155+
tagged_locator('kernel.uri_signer'),
156+
])
157+
->tag('kernel.event_subscriber')
158+
151159
->set('controller.helper', ControllerHelper::class)
152160
->tag('container.service_subscriber')
153161

src/Symfony/Component/HttpFoundation/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Deprecate using `Request::sendHeaders()` after headers have already been sent; use a `StreamedResponse` instead
8+
* Add `#[WithHttpStatus]` to define status codes: 404 for `SignedUriException` and 403 for `ExpiredSignedUriException`
89
* Add support for the `QUERY` HTTP method
910

1011
7.3

src/Symfony/Component/HttpFoundation/Exception/ExpiredSignedUriException.php

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

1212
namespace Symfony\Component\HttpFoundation\Exception;
1313

14+
use Symfony\Component\HttpFoundation\Response;
15+
use Symfony\Component\HttpKernel\Attribute\WithHttpStatus;
16+
1417
/**
1518
* @author Kevin Bond <kevinbond@gmail.com>
1619
*/
20+
#[WithHttpStatus(Response::HTTP_FORBIDDEN)]
1721
final class ExpiredSignedUriException extends SignedUriException
1822
{
1923
/**

src/Symfony/Component/HttpFoundation/Exception/SignedUriException.php

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

1212
namespace Symfony\Component\HttpFoundation\Exception;
1313

14+
use Symfony\Component\HttpFoundation\Response;
15+
use Symfony\Component\HttpKernel\Attribute\WithHttpStatus;
16+
1417
/**
1518
* @author Kevin Bond <kevinbond@gmail.com>
1619
*/
20+
#[WithHttpStatus(Response::HTTP_NOT_FOUND)]
1721
abstract class SignedUriException extends \RuntimeException implements ExceptionInterface
1822
{
1923
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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\HttpKernel\Attribute;
13+
14+
/**
15+
* Validates the request signature for specific HTTP methods.
16+
*
17+
* This class determines whether a request's signature should be validated
18+
* based on the configured HTTP methods. If the request method matches one
19+
* of the specified methods (or if no methods are specified), the signature
20+
* is checked.
21+
*
22+
* If the signature is invalid, a {@see \Symfony\Component\HttpFoundation\Exception\SignedUriException}
23+
* is thrown during validation.
24+
*
25+
* @author Santiago San Martin <sanmartindev@gmail.com>
26+
*/
27+
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
28+
final class IsSignatureValid
29+
{
30+
public readonly array $methods;
31+
public readonly ?string $signer;
32+
33+
/**
34+
* @param string[]|string $methods HTTP methods that require signature validation. An empty array means that no method filtering is done
35+
* @param string $signer The ID of the UriSigner service to use for signature validation. Defaults to 'uri_signer'
36+
*/
37+
public function __construct(
38+
array|string $methods = [],
39+
?string $signer = null,
40+
) {
41+
$this->methods = (array) $methods;
42+
$this->signer = $signer;
43+
}
44+
}

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add support for the `QUERY` HTTP method
88
* Deprecate implementing `__sleep/wakeup()` on kernels; use `__(un)serialize()` instead
99
* Deprecate implementing `__sleep/wakeup()` on data collectors; use `__(un)serialize()` instead
10+
* Add `#[IsSignatureValid]` attribute to validate URI signatures
1011
* Make `Profile` final and `Profiler::__sleep()` internal
1112

1213
7.3
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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\HttpKernel\EventListener;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
16+
use Symfony\Component\HttpFoundation\UriSigner;
17+
use Symfony\Component\HttpKernel\Attribute\IsSignatureValid;
18+
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
19+
use Symfony\Component\HttpKernel\KernelEvents;
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+
private readonly ContainerInterface $container,
31+
) {
32+
}
33+
34+
public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
35+
{
36+
if (!$attributes = $event->getAttributes(IsSignatureValid::class)) {
37+
return;
38+
}
39+
40+
$request = $event->getRequest();
41+
foreach ($attributes as $attribute) {
42+
$methods = array_map('strtoupper', $attribute->methods);
43+
if ($methods && !\in_array($request->getMethod(), $methods, true)) {
44+
continue;
45+
}
46+
47+
if (null === $attribute->signer) {
48+
$this->uriSigner->verify($request);
49+
continue;
50+
}
51+
52+
$signer = $this->container->get($attribute->signer);
53+
if (!$signer instanceof UriSigner) {
54+
throw new \LogicException(\sprintf('The service "%s" is not an instance of "%s".', $attribute->signer, UriSigner::class));
55+
}
56+
57+
$signer->verify($request);
58+
}
59+
}
60+
61+
public static function getSubscribedEvents(): array
62+
{
63+
return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 30]];
64+
}
65+
}

0 commit comments

Comments
 (0)