Skip to content

Commit 2290ba3

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

File tree

13 files changed

+503
-1
lines changed

13 files changed

+503
-1
lines changed

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|null $signer The id of the UriSigner service to use for signature validation or null to use the default 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->getDefinitions() as $id => $definition) {
33+
if (UriSigner::class === $definition->getClass()) {
34+
$locateableServices[$id] = new Reference($id);
35+
}
36+
}
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+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 Psr\Container\ContainerInterface;
15+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
16+
use Symfony\Component\HttpFoundation\UriSigner;
17+
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
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 ContainerInterface $container,
30+
) {
31+
}
32+
33+
public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
34+
{
35+
if (!\is_array($attributes = $event->getAttributes(IsSignatureValid::class) ?? null)) {
36+
return;
37+
}
38+
39+
$request = $event->getRequest();
40+
foreach ($attributes as $attribute) {
41+
$methods = array_map('strtoupper', $attribute->methods);
42+
if ($methods && !\in_array($request->getMethod(), $methods, true)) {
43+
continue;
44+
}
45+
46+
$signer = $this->container->get($attribute->signer);
47+
if (!$signer instanceof UriSigner) {
48+
throw new \LogicException(\sprintf('The service "%s" is not an instance of "%s".', $attribute->signer, UriSigner::class));
49+
}
50+
51+
$signer->verify($request);
52+
}
53+
}
54+
55+
public static function getSubscribedEvents(): array
56+
{
57+
return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 30]];
58+
}
59+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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\Tests\DependencyInjection;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Psr\Container\ContainerInterface;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\HttpFoundation\UriSigner;
18+
use Symfony\Component\Security\Http\DependencyInjection\UriSignerLocatorPass;
19+
20+
class UriSignerLocatorPassTest extends TestCase
21+
{
22+
public function testSetUriSignerLocator()
23+
{
24+
$container = new ContainerBuilder();
25+
26+
$container->register('controller.is_signature_valid_attribute_listener');
27+
28+
$container->register('UriSignerOne')->setClass(UriSigner::class);
29+
$container->register('UriSignerTwo')->setClass(UriSigner::class);
30+
$container->register('UriSignerThree')->setClass(UriSigner::class);
31+
$container->register('One')->setClass('Foo');
32+
$container->register('Two')->setClass('Bar');
33+
$container->register('Three')->setClass('Baz');
34+
35+
$pass = new UriSignerLocatorPass();
36+
$pass->process($container);
37+
38+
$id = (string) $container->getDefinition('controller.is_signature_valid_attribute_listener')->getBindings()[ContainerInterface::class]->getValues()[0];
39+
$arguments = $container->getDefinition($id)->getArguments()[0];
40+
41+
$this->assertCount(3, $arguments);
42+
$this->assertArrayHasKey('UriSignerOne', $arguments);
43+
$this->assertArrayHasKey('UriSignerTwo', $arguments);
44+
$this->assertArrayHasKey('UriSignerThree', $arguments);
45+
}
46+
}

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