mirror of
https://github.com/symfony/http-kernel.git
synced 2026-03-24 01:12:09 +01:00
feature #61458 [HttpKernel] Validate typed request attribute values before calling controllers (mudassaralichouhan)
This PR was merged into the 8.1 branch. Discussion ---------- [HttpKernel] Validate typed request attribute values before calling controllers <!-- This file is part of the Symfony package. (c) Fabien Potencier <fabien@symfony.com> For the full copyright and license information, please view the LICENSE file that was distributed with this source code. --> | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #61451 | License | MIT | Doc PR | N/A This PR fixes issue #61451 by introducing a new `RequestAttributeScalarValueResolver` that safely casts request attributes (route parameters) to scalar types (int, float, bool, string, or \BackedEnum). If a value cannot be safely cast (e.g. `9223372036854775808` for an `int`), a `NotFoundHttpException` (404) is thrown before reaching the controller, avoiding a `TypeError`. ### Changes: - Added: `RequestAttributeScalarValueResolver` (registered with high priority) - Tests: Unit and functional tests for valid, invalid, and out-of-range int values - Config: Functional test app with a `/{id}` route expecting `int $id` - Docs: `CHANGELOG.md` entry under 7.4 This makes route handling safer and more predictable for typed controllers. Commits ------- bb0f715e833 [HttpKernel] Validate typed request attribute values before calling controllers
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -35,16 +35,16 @@ final class BackedEnumValueResolver implements ValueResolverInterface
|
||||
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 @@ final class BackedEnumValueResolver implements ValueResolverInterface
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ 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;
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ class BackedEnumValueResolverTest extends TestCase
|
||||
$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);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user