Skip to content

Commit f20400b

Browse files
committed
[HttpFoundation] Add #[IsSignatureValid] attribute
1 parent 36d8d5d commit f20400b

File tree

17 files changed

+557
-1
lines changed

17 files changed

+557
-1
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 `security.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
@@ -89,6 +89,7 @@ class UnusedTagsPass implements CompilerPassInterface
8989
'security.authenticator.login_linker',
9090
'security.expression_language_provider',
9191
'security.remember_me_handler',
92+
'security.uri_signer',
9293
'security.voter',
9394
'serializer.encoder',
9495
'serializer.normalizer',

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
use Symfony\Component\HttpClient\ThrottlingHttpClient;
9999
use Symfony\Component\HttpClient\UriTemplateHttpClient;
100100
use Symfony\Component\HttpFoundation\Request;
101+
use Symfony\Component\HttpFoundation\UriSigner;
101102
use Symfony\Component\HttpKernel\Attribute\AsController;
102103
use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver;
103104
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
@@ -749,6 +750,8 @@ public function load(array $configs, ContainerBuilder $container): void
749750
->addTag('mime.mime_type_guesser');
750751
$container->registerForAutoconfiguration(LoggerAwareInterface::class)
751752
->addMethodCall('setLogger', [new Reference('logger')]);
753+
$container->registerForAutoconfiguration(UriSigner::class)
754+
->addTag('security.uri_signer');
752755

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

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass;
6565
use Symfony\Component\Runtime\SymfonyRuntime;
6666
use Symfony\Component\Scheduler\DependencyInjection\AddScheduleMessengerPass;
67+
use Symfony\Component\Security\Http\DependencyInjection\UriSignerLocatorPass;
6768
use Symfony\Component\Serializer\DependencyInjection\SerializerPass;
6869
use Symfony\Component\Translation\DependencyInjection\DataCollectorTranslatorPass;
6970
use Symfony\Component\Translation\DependencyInjection\LoggingTranslatorPass;
@@ -192,6 +193,7 @@ public function build(ContainerBuilder $container): void
192193
$container->addCompilerPass(new VirtualRequestStackPass());
193194
$container->addCompilerPass(new TranslationUpdateCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
194195
$this->addCompilerPassIfExists($container, StreamablePass::class);
196+
$this->addCompilerPassIfExists($container, UriSignerLocatorPass::class);
195197

196198
if ($container->getParameter('kernel.debug')) {
197199
$container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2);

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

910
7.3
1011
---

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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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\UriSigner;
15+
16+
/**
17+
* Validates the request signature for specific HTTP methods.
18+
*
19+
* This class determines whether a request's signature should be validated
20+
* based on the configured HTTP methods. If the request method matches one
21+
* of the specified methods (or if no methods are specified), the signature
22+
* is checked.
23+
*
24+
* If the signature is invalid, a {@see \Symfony\Component\HttpFoundation\Exception\SignedUriException}
25+
* is thrown during validation.
26+
*
27+
* @author Santiago San Martin <sanmartindev@gmail.com>
28+
*/
29+
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
30+
final class IsSignatureValid
31+
{
32+
public readonly array $methods;
33+
public readonly string $signer;
34+
35+
/**
36+
* @param string[]|string $methods HTTP methods that require signature validation. An empty array means that no method filtering is done
37+
* @param string $signer The ID of the UriSigner service to use for signature validation. Defaults to 'uri_signer'
38+
*/
39+
public function __construct(
40+
array|string $methods = [],
41+
string $signer = 'uri_signer',
42+
) {
43+
if (!method_exists(UriSigner::class, 'verify')) {
44+
throw new \LogicException('The `IsSignatureValid` attribute requires symfony/http-foundation >= 7.3.');
45+
}
46+
47+
$this->methods = (array) $methods;
48+
$this->signer = $signer;
49+
}
50+
}

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

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

77
* Deprecate callable firewall listeners, extend `AbstractListener` or implement `FirewallListenerInterface` instead
88
* Deprecate `AbstractListener::__invoke`
9+
* Add `#[IsSignatureValid]` attribute to validate URI signatures
910

1011
7.3
1112
---
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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\DependencyInjection;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16+
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
17+
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
use Symfony\Component\DependencyInjection\Reference;
19+
use Symfony\Component\HttpFoundation\UriSigner;
20+
use Symfony\Component\Security\Http\EventListener\IsSignatureValidAttributeListener;
21+
22+
/**
23+
* Registers all UriSigner services in a service locator and injects it into the IsSignatureValidAttributeListener for dynamic signer resolution.
24+
*
25+
* @author Santiago San Martin <sanmartindev@gmail.com>
26+
*/
27+
class UriSignerLocatorPass implements CompilerPassInterface
28+
{
29+
public function process(ContainerBuilder $container): void
30+
{
31+
$locateableServices = [];
32+
foreach ($container->findTaggedServiceIds('security.uri_signer') as $id => $attributes) {
33+
$locateableServices[$id] = new Reference($id);
34+
}
35+
36+
$locateableServices['uri_signer'] = new Reference(UriSigner::class);
37+
38+
$container
39+
->register('controller.is_signature_valid_attribute_listener', IsSignatureValidAttributeListener::class)
40+
->addTag('kernel.event_subscriber')
41+
->setBindings([
42+
ContainerInterface::class => ServiceLocatorTagPass::register($container, $locateableServices),
43+
]);
44+
}
45+
}

0 commit comments

Comments
 (0)