diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 33fb862c1fbd9..970a5d592326e 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 8.1 --- + * Validate typed route parameters before calling controllers and return an HTTP error when an invalid value is provided * Add `ControllerAttributeEvent` et al. to dispatch events named after controller attributes * Add support for `UploadedFile` when using `MapRequestPayload` * Add support for bundles as compiler pass diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php index 9193cee060f69..764605bfc9f3d 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php @@ -35,16 +35,16 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable return []; } + $name = $argument->getName(); + // do not support if no value can be resolved at all // letting the \Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver be used // or \Symfony\Component\HttpKernel\Controller\ArgumentResolver fail with a meaningful error. - if (!$request->attributes->has($argument->getName())) { + if (!$request->attributes->has($name)) { return []; } - $value = $request->attributes->get($argument->getName()); - - if (null === $value) { + if (null === $value = $request->attributes->get($name)) { return [null]; } @@ -52,17 +52,17 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable return [$value]; } + /** @var class-string<\BackedEnum> $type */ + $type = $argument->getType(); + if (!\is_int($value) && !\is_string($value)) { - throw new \LogicException(\sprintf('Could not resolve the "%s $%s" controller argument: expecting an int or string, got "%s".', $argument->getType(), $argument->getName(), get_debug_type($value))); + throw new NotFoundHttpException(\sprintf('Could not resolve the "%s $%s" controller argument: expecting an int or string, got "%s".', $type, $name, get_debug_type($value))); } - /** @var class-string<\BackedEnum> $enumType */ - $enumType = $argument->getType(); - try { - return [$enumType::from($value)]; + return [$type::from($value)]; } catch (\ValueError|\TypeError $e) { - throw new NotFoundHttpException(\sprintf('Could not resolve the "%s $%s" controller argument: ', $argument->getType(), $argument->getName()).$e->getMessage(), $e); + throw new NotFoundHttpException(\sprintf('Could not resolve the "%s $%s" controller argument: ', $type, $name).$e->getMessage(), $e); } } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php index 2a8d48ee30174..02d3f5868ffc8 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,45 @@ 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 []; + } + + $value = $request->attributes->get($name); + + if (null === $value && $argument->isNullable()) { + return [null]; + } + + $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]; + } + + if ('string' === $type) { + if (!\is_scalar($value) && !$value instanceof \Stringable) { + throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); + } + + $value = (string) $value; + } elseif ($filter = match ($type) { + 'int' => \FILTER_VALIDATE_INT, + 'float' => \FILTER_VALIDATE_FLOAT, + 'bool' => \FILTER_VALIDATE_BOOL, + default => null, + }) { + if (null === $value = $request->attributes->filter($name, null, $filter, ['flags' => \FILTER_NULL_ON_FAILURE | \FILTER_REQUIRE_SCALAR])) { + throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); + } + } + + return [$value]; } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php index 9273f6f55035c..7e7b2970a9a91 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php @@ -119,7 +119,7 @@ public function testResolveThrowsOnUnexpectedType() $request = self::createRequest(['suit' => false]); $metadata = self::createArgumentMetadata('suit', Suit::class); - $this->expectException(\LogicException::class); + $this->expectException(NotFoundHttpException::class); $this->expectExceptionMessage('Could not resolve the "Symfony\Component\HttpKernel\Tests\Fixtures\Suit $suit" controller argument: expecting an int or string, got "bool".'); $resolver->resolve($request, $metadata); diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeValueResolverTest.php new file mode 100644 index 0000000000000..e22bcbf205d30 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestAttributeValueResolverTest.php @@ -0,0 +1,68 @@ + + * + * 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; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +class RequestAttributeValueResolverTest extends TestCase +{ + public function testValidIntWithinRangeWorks() + { + $resolver = new RequestAttributeValueResolver(); + $request = new Request(); + $request->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() + { + $resolver = new RequestAttributeValueResolver(); + $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() + { + $resolver = new RequestAttributeValueResolver(); + $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)); + } + + public function testNullableIntAllowsNull() + { + $resolver = new RequestAttributeValueResolver(); + $request = new Request(); + $request->attributes->set('id', null); + $metadata = new ArgumentMetadata('id', 'int', false, true, null); + + $result = iterator_to_array($resolver->resolve($request, $metadata)); + + $this->assertSame([null], $result); + } +}