From d35ff3a5990d3544d6ddc89e47531b2051cb1c39 Mon Sep 17 00:00:00 2001 From: Mudassar Date: Tue, 19 Aug 2025 16:53:28 +0500 Subject: [PATCH 1/4] Prevent type error on invalid or out-of-range route params by adding RequestAttributeScalarValueResolver - Added RequestAttributeScalarValueResolver to safely resolve typed scalar arguments and throw 404 on invalid input - Registered resolver with high priority before raw attribute resolver - Added unit and functional tests for int route param edge cases - Documented behavior in Routing CHANGELOG --- .../FrameworkBundle/Resources/config/web.php | 4 + ...ibuteScalarValueResolverFunctionalTest.php | 50 ++++++++ ...ibuteScalarValueResolverTestController.php | 26 ++++ .../bundles.php | 18 +++ .../config.yml | 4 + .../routing.yml | 5 + .../Controller/ArgumentResolver.php | 1 + .../RequestAttributeScalarValueResolver.php | 113 ++++++++++++++++++ ...equestAttributeScalarValueResolverTest.php | 49 ++++++++ src/Symfony/Component/Routing/CHANGELOG.md | 3 + 10 files changed, 273 insertions(+) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverFunctionalTest.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverTestController.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/bundles.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/config.yml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/routing.yml create mode 100644 src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeScalarValueResolver.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeScalarValueResolverTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php index 29e1287156398..92257b695c557 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php @@ -19,6 +19,7 @@ use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeScalarValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParameterValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; @@ -55,6 +56,9 @@ abstract_arg('targeted value resolvers'), ]) + ->set('argument_resolver.request_attribute_scalar', RequestAttributeScalarValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 150, 'name' => RequestAttributeScalarValueResolver::class]) + ->set('argument_resolver.backed_enum_resolver', BackedEnumValueResolver::class) ->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => BackedEnumValueResolver::class]) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverFunctionalTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverFunctionalTest.php new file mode 100644 index 0000000000000..b487703474b3f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverFunctionalTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\KernelBrowser; + +class RequestAttributeScalarValueResolverFunctionalTest extends AbstractWebTestCase +{ + public function testValidIntReturns200(): void + { + $client = $this->createClient(['test_case' => 'RequestAttributeScalarValueResolver']); + + $client->request('GET', '/123'); + $response = $client->getResponse(); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('123', $response->getContent()); + } + + public function testInvalidStringReturns404(): void + { + $client = $this->createClient(['test_case' => 'RequestAttributeScalarValueResolver']); + + $client->request('GET', '/abc'); + $response = $client->getResponse(); + + $this->assertSame(404, $response->getStatusCode()); + } + + public function testOutOfRangeIntReturns404(): void + { + $client = $this->createClient(['test_case' => 'RequestAttributeScalarValueResolver']); + + $client->request('GET', '/9223372036854775808'); + $response = $client->getResponse(); + + $this->assertSame(404, $response->getStatusCode()); + } +} + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverTestController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverTestController.php new file mode 100644 index 0000000000000..72b98279312d6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverTestController.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +class RequestAttributeScalarValueResolverTestController +{ + #[Route(path: '/{id}', name: 'test_scalar_id')] + public function __invoke(int $id): Response + { + return new Response((string) $id); + } +} + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/bundles.php new file mode 100644 index 0000000000000..b47123274031d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/bundles.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; + +return [ + new FrameworkBundle(), +]; + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/config.yml new file mode 100644 index 0000000000000..435512a01d340 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/config.yml @@ -0,0 +1,4 @@ +imports: + - { resource: ../config/default.yml } + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/routing.yml new file mode 100644 index 0000000000000..61310b2e4d6b2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/routing.yml @@ -0,0 +1,5 @@ +controllers: + resource: Symfony\Bundle\FrameworkBundle\Tests\Functional\RequestAttributeScalarValueResolverTestController + type: attribute + + diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php index b09a92f02dab3..3ba1e0e63b8e2 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php @@ -131,6 +131,7 @@ public function getArguments(Request $request, callable $controller, ?\Reflectio public static function getDefaultArgumentValueResolvers(): iterable { return [ + new ArgumentResolver\RequestAttributeScalarValueResolver(), new RequestAttributeValueResolver(), new RequestValueResolver(), new SessionValueResolver(), diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeScalarValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeScalarValueResolver.php new file mode 100644 index 0000000000000..c4ea75af1e86c --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeScalarValueResolver.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +/** + * Safely resolves typed scalar and BackedEnum arguments from request attributes (path parameters). + * + * If a value cannot be converted to the expected type (e.g. invalid int or out-of-range), + * a 404 Not Found is thrown instead of letting execution reach the controller and erroring. + * + * Types handled: string, int, float, bool, \BackedEnum + */ +final class RequestAttributeScalarValueResolver implements ValueResolverInterface +{ + public function resolve(Request $request, ArgumentMetadata $argument): array + { + if ($argument->isVariadic()) { + return []; + } + + $name = $argument->getName(); + if (!$request->attributes->has($name)) { + return []; + } + + $type = $argument->getType(); + if (null === $type) { + // Untyped; let the default attribute resolver handle it. + return []; + } + + // Skip union or intersection types; fall back to default behavior + if (str_contains($type, '|') || str_contains($type, '&')) { + return []; + } + + $value = $request->attributes->get($name); + + // Handle Backed Enum typed arguments + if (is_subclass_of($type, \BackedEnum::class, true)) { + if (!\is_string($value) && !\is_int($value)) { + throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); + } + try { + return [$type::from($value)]; + } catch (\ValueError) { + throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); + } + } + + if ($value instanceof \Stringable) { + $value = (string) $value; + } + + switch ($type) { + case 'string': + if (\is_scalar($value) || (null === $value && $argument->isNullable())) { + return [(string) $value]; + } + throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); + + case 'int': + if (\is_int($value)) { + return [$value]; + } + $filtered = filter_var($value, \FILTER_VALIDATE_INT, \FILTER_NULL_ON_FAILURE); + if (null === $filtered) { + // Includes out-of-range values + throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); + } + return [$filtered]; + + case 'float': + if (\is_float($value)) { + return [$value]; + } + $filtered = filter_var($value, \FILTER_VALIDATE_FLOAT, \FILTER_NULL_ON_FAILURE); + if (null === $filtered) { + throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); + } + return [$filtered]; + + case 'bool': + if (\is_bool($value)) { + return [$value]; + } + $filtered = filter_var($value, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE); + if (null === $filtered) { + throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); + } + return [$filtered]; + } + + // Unknown type; let other resolvers handle it (e.g., Request, Session, custom types) + return []; + } +} + + diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeScalarValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeScalarValueResolverTest.php new file mode 100644 index 0000000000000..d5af4ff5bbe77 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeScalarValueResolverTest.php @@ -0,0 +1,49 @@ +attributes->set('id', '123'); + $metadata = new ArgumentMetadata('id', 'int', false, false, null); + + $result = iterator_to_array($resolver->resolve($request, $metadata)); + + $this->assertSame([123], $result); + } + + public function testInvalidStringBecomes404(): void + { + $resolver = new RequestAttributeScalarValueResolver(); + $request = new Request(); + $request->attributes->set('id', 'abc'); + $metadata = new ArgumentMetadata('id', 'int', false, false, null); + + $this->expectException(NotFoundHttpException::class); + iterator_to_array($resolver->resolve($request, $metadata)); + } + + public function testOutOfRangeIntBecomes404(): void + { + $resolver = new RequestAttributeScalarValueResolver(); + $request = new Request(); + // one more than PHP_INT_MAX on 64-bit (string input) + $request->attributes->set('id', '9223372036854775808'); + $metadata = new ArgumentMetadata('id', 'int', false, false, null); + + $this->expectException(NotFoundHttpException::class); + iterator_to_array($resolver->resolve($request, $metadata)); + } +} + + diff --git a/src/Symfony/Component/Routing/CHANGELOG.md b/src/Symfony/Component/Routing/CHANGELOG.md index 4ef96d53232fe..28b1bff0a83c6 100644 --- a/src/Symfony/Component/Routing/CHANGELOG.md +++ b/src/Symfony/Component/Routing/CHANGELOG.md @@ -5,6 +5,9 @@ CHANGELOG --- * Allow query-specific parameters in `UrlGenerator` using `_query` + * Validate typed path parameters before controller invocation: when a route parameter is bound to + a typed controller argument (int, float, bool, string, or \BackedEnum), invalid or out-of-range + values now result in a 404 Not Found instead of triggering a TypeError in the controller. 7.3 --- From 7b01081f7a1e8e5a9233771533aa4d5d1fb4899f Mon Sep 17 00:00:00 2001 From: Mudassar Date: Tue, 19 Aug 2025 19:56:30 +0500 Subject: [PATCH 2/4] CS: remove :void from test methods per fixer rules --- ...tributeScalarValueResolverFunctionalTest.php | 10 +++------- .../RequestAttributeScalarValueResolverTest.php | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverFunctionalTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverFunctionalTest.php index b487703474b3f..96d7e028f6e51 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverFunctionalTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverFunctionalTest.php @@ -11,11 +11,9 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; -use Symfony\Bundle\FrameworkBundle\KernelBrowser; - class RequestAttributeScalarValueResolverFunctionalTest extends AbstractWebTestCase { - public function testValidIntReturns200(): void + public function testValidIntReturns200() { $client = $this->createClient(['test_case' => 'RequestAttributeScalarValueResolver']); @@ -26,7 +24,7 @@ public function testValidIntReturns200(): void $this->assertSame('123', $response->getContent()); } - public function testInvalidStringReturns404(): void + public function testInvalidStringReturns404() { $client = $this->createClient(['test_case' => 'RequestAttributeScalarValueResolver']); @@ -36,7 +34,7 @@ public function testInvalidStringReturns404(): void $this->assertSame(404, $response->getStatusCode()); } - public function testOutOfRangeIntReturns404(): void + public function testOutOfRangeIntReturns404() { $client = $this->createClient(['test_case' => 'RequestAttributeScalarValueResolver']); @@ -46,5 +44,3 @@ public function testOutOfRangeIntReturns404(): void $this->assertSame(404, $response->getStatusCode()); } } - - diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeScalarValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeScalarValueResolverTest.php index d5af4ff5bbe77..00f1da9346409 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeScalarValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeScalarValueResolverTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\TestCase; @@ -10,7 +19,7 @@ class RequestAttributeScalarValueResolverTest extends TestCase { - public function testValidIntWithinRangeWorks(): void + public function testValidIntWithinRangeWorks() { $resolver = new RequestAttributeScalarValueResolver(); $request = new Request(); @@ -22,7 +31,7 @@ public function testValidIntWithinRangeWorks(): void $this->assertSame([123], $result); } - public function testInvalidStringBecomes404(): void + public function testInvalidStringBecomes404() { $resolver = new RequestAttributeScalarValueResolver(); $request = new Request(); @@ -33,7 +42,7 @@ public function testInvalidStringBecomes404(): void iterator_to_array($resolver->resolve($request, $metadata)); } - public function testOutOfRangeIntBecomes404(): void + public function testOutOfRangeIntBecomes404() { $resolver = new RequestAttributeScalarValueResolver(); $request = new Request(); @@ -45,5 +54,3 @@ public function testOutOfRangeIntBecomes404(): void iterator_to_array($resolver->resolve($request, $metadata)); } } - - From 736e9ddea0fea7a126e3e643d32d796a41d8c7f3 Mon Sep 17 00:00:00 2001 From: Mudassar Date: Tue, 19 Aug 2025 19:57:14 +0500 Subject: [PATCH 3/4] Fix coding style issues with PHP-CS-Fixer --- src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php | 2 +- .../RequestAttributeScalarValueResolverTestController.php | 2 -- .../app/RequestAttributeScalarValueResolver/bundles.php | 2 -- .../RequestAttributeScalarValueResolver.php | 6 +++--- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php index 92257b695c557..2634ebec83610 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php @@ -19,8 +19,8 @@ use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeScalarValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParameterValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeScalarValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverTestController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverTestController.php index 72b98279312d6..1665bb51435bb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverTestController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RequestAttributeScalarValueResolverTestController.php @@ -22,5 +22,3 @@ public function __invoke(int $id): Response return new Response((string) $id); } } - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/bundles.php index b47123274031d..13ab9fddee4a6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/bundles.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/RequestAttributeScalarValueResolver/bundles.php @@ -14,5 +14,3 @@ return [ new FrameworkBundle(), ]; - - diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeScalarValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeScalarValueResolver.php index c4ea75af1e86c..dc2105eff4244 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeScalarValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeScalarValueResolver.php @@ -72,7 +72,6 @@ public function resolve(Request $request, ArgumentMetadata $argument): array return [(string) $value]; } throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); - case 'int': if (\is_int($value)) { return [$value]; @@ -82,6 +81,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array // Includes out-of-range values throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); } + return [$filtered]; case 'float': @@ -92,6 +92,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array if (null === $filtered) { throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); } + return [$filtered]; case 'bool': @@ -102,6 +103,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array if (null === $filtered) { throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); } + return [$filtered]; } @@ -109,5 +111,3 @@ public function resolve(Request $request, ArgumentMetadata $argument): array return []; } } - - From 07701bce8b9aa62f03939f6c10524e665e08818a Mon Sep 17 00:00:00 2001 From: Mudassar Date: Tue, 19 Aug 2025 23:43:21 +0500 Subject: [PATCH 4/4] Refactor: use ParameterBag::filter() for scalar type validation in RequestAttributeValueResolver --- .../FrameworkBundle/Resources/config/web.php | 4 -- .../Controller/ArgumentResolver.php | 1 - .../RequestAttributeValueResolver.php | 58 ++++++++++++++++++- ...equestAttributeScalarValueResolverTest.php | 8 +-- 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php index 2634ebec83610..29e1287156398 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php @@ -20,7 +20,6 @@ use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParameterValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeScalarValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; @@ -56,9 +55,6 @@ abstract_arg('targeted value resolvers'), ]) - ->set('argument_resolver.request_attribute_scalar', RequestAttributeScalarValueResolver::class) - ->tag('controller.argument_value_resolver', ['priority' => 150, 'name' => RequestAttributeScalarValueResolver::class]) - ->set('argument_resolver.backed_enum_resolver', BackedEnumValueResolver::class) ->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => BackedEnumValueResolver::class]) diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php index 3ba1e0e63b8e2..b09a92f02dab3 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php @@ -131,7 +131,6 @@ public function getArguments(Request $request, callable $controller, ?\Reflectio public static function getDefaultArgumentValueResolvers(): iterable { return [ - new ArgumentResolver\RequestAttributeScalarValueResolver(), new RequestAttributeValueResolver(), new RequestValueResolver(), new SessionValueResolver(), diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php index 2a8d48ee30174..047a85b2b6570 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php @@ -14,6 +14,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Yields a non-variadic argument's value from the request attributes. @@ -24,6 +25,61 @@ final class RequestAttributeValueResolver implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): array { - return !$argument->isVariadic() && $request->attributes->has($argument->getName()) ? [$request->attributes->get($argument->getName())] : []; + if ($argument->isVariadic()) { + return []; + } + + $name = $argument->getName(); + if (!$request->attributes->has($name)) { + return []; + } + + $type = $argument->getType(); + + // Skip when no type declaration or complex types; fall back to other resolvers/defaults + if (null === $type || str_contains($type, '|') || str_contains($type, '&')) { + return [$value]; + } + + // Let the dedicated resolver handle backed enums + if (is_subclass_of($type, \BackedEnum::class, true)) { + return []; + } + + // Only enforce safe casting for int/float/bool here using ParameterBag::filter() + switch ($type) { + case 'int': + $filtered = $request->attributes->filter($name, null, \FILTER_VALIDATE_INT, [ + 'flags' => \FILTER_NULL_ON_FAILURE | \FILTER_REQUIRE_SCALAR, + ]); + if (null === $filtered) { + throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); + } + + return [$filtered]; + + case 'float': + $filtered = $request->attributes->filter($name, null, \FILTER_VALIDATE_FLOAT, [ + 'flags' => \FILTER_NULL_ON_FAILURE | \FILTER_REQUIRE_SCALAR, + ]); + if (null === $filtered) { + throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); + } + + return [$filtered]; + + case 'bool': + $filtered = $request->attributes->filter($name, null, \FILTER_VALIDATE_BOOL, [ + 'flags' => \FILTER_NULL_ON_FAILURE | \FILTER_REQUIRE_SCALAR, + ]); + if (null === $filtered) { + throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); + } + + return [$filtered]; + } + + // Strings and any other types: keep default behavior + return [$request->attributes->get($name)]; } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeScalarValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeScalarValueResolverTest.php index 00f1da9346409..de0b55d2d99d5 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeScalarValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeScalarValueResolverTest.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeScalarValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -21,7 +21,7 @@ class RequestAttributeScalarValueResolverTest extends TestCase { public function testValidIntWithinRangeWorks() { - $resolver = new RequestAttributeScalarValueResolver(); + $resolver = new RequestAttributeValueResolver(); $request = new Request(); $request->attributes->set('id', '123'); $metadata = new ArgumentMetadata('id', 'int', false, false, null); @@ -33,7 +33,7 @@ public function testValidIntWithinRangeWorks() public function testInvalidStringBecomes404() { - $resolver = new RequestAttributeScalarValueResolver(); + $resolver = new RequestAttributeValueResolver(); $request = new Request(); $request->attributes->set('id', 'abc'); $metadata = new ArgumentMetadata('id', 'int', false, false, null); @@ -44,7 +44,7 @@ public function testInvalidStringBecomes404() public function testOutOfRangeIntBecomes404() { - $resolver = new RequestAttributeScalarValueResolver(); + $resolver = new RequestAttributeValueResolver(); $request = new Request(); // one more than PHP_INT_MAX on 64-bit (string input) $request->attributes->set('id', '9223372036854775808');