Skip to content

Commit de2a7ab

Browse files
committed
bug symfony#60296 [Serializer] Handle invalid mapping type property type (KorvinSzanto)
This PR was squashed before being merged into the 7.2 branch. Discussion ---------- [Serializer] Handle invalid mapping type property type | Q | A | ------------- | --- | Branch? | 7.2 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | | License | MIT When using `#[MapRequestPayload]` along with a type that uses a `#[DescriminatorMap]` it's possible for a user to craft a payload that triggers a `TypeError` by passing the wrong type for the "type" property. For example, a class that has: ```php #[DiscriminatorMap('field', ['a' => AController::class, 'b' => BController::class])] ``` and a request comes in with: ``` Content-Type: application/json {"field":{}} ``` will trigger a 500 because `AbstractObjectNormalizer` doesn't validate the field type before passing it to `->getClassForType` which typehints for string. This PR adds a conditional that filters anything other than strings or objects that have a __toString method. Commits ------- 6ab0182 [Serializer] Handle invalid mapping type property type
2 parents 00f7c07 + 6ab0182 commit de2a7ab

File tree

4 files changed

+73
-1
lines changed

4 files changed

+73
-1
lines changed

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,10 @@ private function getMappedClass(array $data, string $class, array $context): str
11841184
throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false);
11851185
}
11861186

1187+
if (!\is_string($type) && (!\is_object($type) || !method_exists($type, '__toString'))) {
1188+
throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type property "%s" for the abstract object "%s" must be a string or a stringable object.', $mapping->getTypeProperty(), $class), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false);
1189+
}
1190+
11871191
if (null === $mappedClass = $mapping->getClassForType($type)) {
11881192
throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true);
11891193
}

src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/AbstractDummyFirstChild.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
class AbstractDummyFirstChild extends AbstractDummy
1717
{
1818
public $bar;
19+
public $baz;
1920

2021
/** @var DummyFirstChildQuux|null */
2122
public $quux;

src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,73 @@ private function getDenormalizerForStringCollection()
528528
return $denormalizer;
529529
}
530530

531+
/**
532+
* @dataProvider provideInvalidDiscriminatorTypes
533+
*/
534+
public function testDenormalizeWithDiscriminatorMapHandlesInvalidTypeValue(mixed $typeValue, bool $shouldFail)
535+
{
536+
if ($shouldFail) {
537+
$this->expectException(NotNormalizableValueException::class);
538+
$this->expectExceptionMessage(
539+
'The type property "type" for the abstract object "Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummy" must be a string or a stringable object.'
540+
);
541+
}
542+
543+
$factory = new ClassMetadataFactory(new AttributeLoader());
544+
545+
$loaderMock = new class implements ClassMetadataFactoryInterface {
546+
public function getMetadataFor($value): ClassMetadataInterface
547+
{
548+
if (AbstractDummy::class === $value) {
549+
return new ClassMetadata(
550+
AbstractDummy::class,
551+
new ClassDiscriminatorMapping('type', [
552+
'first' => AbstractDummyFirstChild::class,
553+
'second' => AbstractDummySecondChild::class,
554+
])
555+
);
556+
}
557+
558+
throw new InvalidArgumentException();
559+
}
560+
561+
public function hasMetadataFor($value): bool
562+
{
563+
return AbstractDummy::class === $value;
564+
}
565+
};
566+
567+
$discriminatorResolver = new ClassDiscriminatorFromClassMetadata($loaderMock);
568+
$normalizer = new AbstractObjectNormalizerDummy($factory, null, new ReflectionExtractor(), $discriminatorResolver);
569+
$serializer = new Serializer([$normalizer]);
570+
$normalizer->setSerializer($serializer);
571+
$normalizedData = $normalizer->denormalize(['foo' => 'foo', 'baz' => 'baz', 'quux' => ['value' => 'quux'], 'type' => $typeValue], AbstractDummy::class);
572+
573+
$this->assertInstanceOf(DummyFirstChildQuux::class, $normalizedData->quux);
574+
}
575+
576+
/**
577+
* @return iterable<array{0: mixed, 1: bool}>
578+
*/
579+
public static function provideInvalidDiscriminatorTypes(): array
580+
{
581+
$toStringObject = new class {
582+
public function __toString()
583+
{
584+
return 'first';
585+
}
586+
};
587+
588+
return [
589+
[[], true],
590+
[new \stdClass(), true],
591+
[123, true],
592+
[false, true],
593+
['first', false],
594+
[$toStringObject, false],
595+
];
596+
}
597+
531598
public function testDenormalizeWithDiscriminatorMapUsesCorrectClassname()
532599
{
533600
$factory = new ClassMetadataFactory(new AttributeLoader());

src/Symfony/Component/Serializer/Tests/SerializerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ public function hasMetadataFor($value): bool
452452
$discriminatorResolver = new ClassDiscriminatorFromClassMetadata($loaderMock);
453453
$serializer = new Serializer([new ObjectNormalizer(null, null, null, new PhpDocExtractor(), $discriminatorResolver)], ['json' => new JsonEncoder()]);
454454

455-
$jsonData = '{"type":"first","quux":{"value":"quux"},"bar":"bar-value","foo":"foo-value"}';
455+
$jsonData = '{"type":"first","quux":{"value":"quux"},"bar":"bar-value","baz":null,"foo":"foo-value"}';
456456

457457
$deserialized = $serializer->deserialize($jsonData, AbstractDummy::class, 'json');
458458
$this->assertEquals($example, $deserialized);

0 commit comments

Comments
 (0)