Skip to content

Commit ee9ccde

Browse files
committed
[Serializer] Fix unexpected allowed attributes
1 parent fa23eb6 commit ee9ccde

File tree

9 files changed

+210
-36
lines changed

9 files changed

+210
-36
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@
137137
service('property_info')->ignoreOnInvalid(),
138138
service('serializer.mapping.class_discriminator_resolver')->ignoreOnInvalid(),
139139
null,
140+
[],
141+
service('property_info')->ignoreOnInvalid(),
140142
])
141143

142144
->alias(PropertyNormalizer::class, 'serializer.normalizer.property')

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,10 @@ protected function handleCircularReference(object $object, string $format = null
217217
*
218218
* @throws LogicException if the 'allow_extra_attributes' context variable is false and no class metadata factory is provided
219219
*/
220-
protected function getAllowedAttributes($classOrObject, array $context, bool $attributesAsString = false)
220+
protected function getAllowedAttributes($classOrObject, array $context, bool $attributesAsString = false /* , bool $read = true */)
221221
{
222+
$read = \func_get_args()[3] ?? true;
223+
222224
$allowExtraAttributes = $context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES];
223225
if (!$this->classMetadataFactory) {
224226
if (!$allowExtraAttributes) {
@@ -241,7 +243,7 @@ protected function getAllowedAttributes($classOrObject, array $context, bool $at
241243
if (
242244
!$ignore &&
243245
([] === $groups || array_intersect(array_merge($attributeMetadata->getGroups(), ['*']), $groups)) &&
244-
$this->isAllowedAttribute($classOrObject, $name = $attributeMetadata->getName(), null, $context)
246+
$this->isAllowedAttribute($classOrObject, $name = $attributeMetadata->getName(), null, $context, $read)
245247
) {
246248
$allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata;
247249
}
@@ -269,7 +271,7 @@ protected function getGroups(array $context): array
269271
*
270272
* @return bool
271273
*/
272-
protected function isAllowedAttribute($classOrObject, string $attribute, string $format = null, array $context = [])
274+
protected function isAllowedAttribute($classOrObject, string $attribute, string $format = null, array $context = [] /* , bool $read = true */)
273275
{
274276
$ignoredAttributes = $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES];
275277
if (\in_array($attribute, $ignoredAttributes)) {
@@ -360,7 +362,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex
360362
$context['deserialization_path'] = $objectDeserializationPath ? $objectDeserializationPath.'.'.$paramName : $paramName;
361363

362364
$allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes);
363-
$ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
365+
$ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context, false);
364366
if ($constructorParameter->isVariadic()) {
365367
if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
366368
if (!\is_array($data[$key])) {

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ public function normalize($object, string $format = null, array $context = [])
159159

160160
$data = [];
161161
$stack = [];
162-
$attributes = $this->getAttributes($object, $format, $context);
162+
$attributes = $this->getAttributes($object, $format, $context, true);
163163
$class = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
164164
$attributesMetadata = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
165165
if (isset($context[self::MAX_DEPTH_HANDLER])) {
@@ -300,8 +300,10 @@ protected function instantiateObject(array &$data, string $class, array &$contex
300300
*
301301
* @return string[]
302302
*/
303-
protected function getAttributes(object $object, ?string $format, array $context)
303+
protected function getAttributes(object $object, ?string $format, array $context /* , bool $read = true */)
304304
{
305+
$read = \func_get_args()[3] ?? true;
306+
305307
$class = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
306308
$key = $class.'-'.$context['cache_key'];
307309

@@ -319,7 +321,7 @@ protected function getAttributes(object $object, ?string $format, array $context
319321
return $allowedAttributes;
320322
}
321323

322-
$attributes = $this->extractAttributes($object, $format, $context);
324+
$attributes = $this->extractAttributes($object, $format, $context, $read);
323325

324326
if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
325327
array_unshift($attributes, $mapping->getTypeProperty());
@@ -337,7 +339,7 @@ protected function getAttributes(object $object, ?string $format, array $context
337339
*
338340
* @return string[]
339341
*/
340-
abstract protected function extractAttributes(object $object, string $format = null, array $context = []);
342+
abstract protected function extractAttributes(object $object, string $format = null, array $context = [] /* , bool $read = true */);
341343

342344
/**
343345
* Gets the attribute value.
@@ -384,7 +386,7 @@ public function denormalize($data, string $type, string $format = null, array $c
384386

385387
$attributeContext = $this->getAttributeDenormalizationContext($resolvedClass, $attribute, $context);
386388

387-
if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($resolvedClass, $attribute, $format, $context)) {
389+
if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($resolvedClass, $attribute, $format, $context, false)) {
388390
if (!($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES])) {
389391
$extraAttributes[] = $attribute;
390392
}

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

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,23 @@
3636
*/
3737
class GetSetMethodNormalizer extends AbstractObjectNormalizer
3838
{
39+
private static $reflectionCache = [];
3940
private static $setterAccessibleCache = [];
4041

4142
/**
4243
* {@inheritdoc}
4344
*/
4445
public function supportsNormalization($data, string $format = null)
4546
{
46-
return parent::supportsNormalization($data, $format) && $this->supports(\get_class($data));
47+
return parent::supportsNormalization($data, $format) && $this->supports(\get_class($data), true);
4748
}
4849

4950
/**
5051
* {@inheritdoc}
5152
*/
5253
public function supportsDenormalization($data, string $type, string $format = null)
5354
{
54-
return parent::supportsDenormalization($data, $type, $format) && $this->supports($type);
55+
return parent::supportsDenormalization($data, $type, $format) && $this->supports($type, false);
5556
}
5657

5758
/**
@@ -63,22 +64,28 @@ public function hasCacheableSupportsMethod(): bool
6364
}
6465

6566
/**
66-
* Checks if the given class has any getter method.
67+
* Checks if the given class has any getter or setter method.
6768
*/
68-
private function supports(string $class): bool
69+
private function supports(string $class, bool $read): bool
6970
{
7071
if (null !== $this->classDiscriminatorResolver && $this->classDiscriminatorResolver->getMappingForClass($class)) {
7172
return true;
7273
}
7374

74-
$class = new \ReflectionClass($class);
75-
$methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
76-
foreach ($methods as $method) {
77-
if ($this->isGetMethod($method)) {
78-
return true;
79-
}
75+
if (!isset(self::$reflectionCache[$class])) {
76+
self::$reflectionCache[$class] = new \ReflectionClass($class);
8077
}
8178

79+
$reflection = self::$reflectionCache[$class];
80+
81+
do {
82+
foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
83+
if ($read && $this->isGetMethod($reflectionMethod) || !$read && $this->isSetMethod($reflectionMethod)) {
84+
return true;
85+
}
86+
}
87+
} while ($reflection = $reflection->getParentClass());
88+
8289
return false;
8390
}
8491

@@ -95,11 +102,24 @@ private function isGetMethod(\ReflectionMethod $method): bool
95102
);
96103
}
97104

105+
/**
106+
* Checks if a method's name matches /^set.+$/ and can be called non-statically with one parameter.
107+
*/
108+
private function isSetMethod(\ReflectionMethod $method): bool
109+
{
110+
return !$method->isStatic()
111+
&& (\PHP_VERSION_ID < 80000 || !$method->getAttributes(Ignore::class))
112+
&& 1 === $method->getNumberOfRequiredParameters()
113+
&& str_starts_with($method->name, 'set');
114+
}
115+
98116
/**
99117
* {@inheritdoc}
100118
*/
101-
protected function extractAttributes(object $object, string $format = null, array $context = [])
119+
protected function extractAttributes(object $object, string $format = null, array $context = [] /* , bool $read = true */)
102120
{
121+
$read = \func_get_args()[3] ?? true;
122+
103123
$reflectionObject = new \ReflectionObject($object);
104124
$reflectionMethods = $reflectionObject->getMethods(\ReflectionMethod::IS_PUBLIC);
105125

@@ -111,7 +131,7 @@ protected function extractAttributes(object $object, string $format = null, arra
111131

112132
$attributeName = lcfirst(substr($method->name, str_starts_with($method->name, 'is') ? 2 : 3));
113133

114-
if ($this->isAllowedAttribute($object, $attributeName, $format, $context)) {
134+
if ($this->isAllowedAttribute($object, $attributeName, $format, $context, $read)) {
115135
$attributes[] = $attributeName;
116136
}
117137
}
@@ -160,4 +180,51 @@ protected function setAttributeValue(object $object, string $attribute, $value,
160180
$object->$setter($value);
161181
}
162182
}
183+
184+
protected function isAllowedAttribute($classOrObject, string $attribute, string $format = null, array $context = [] /* , bool $read = true */)
185+
{
186+
$read = \func_get_args()[4] ?? true;
187+
188+
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context, $read)) {
189+
return false;
190+
}
191+
192+
$class = \is_object($classOrObject) ? \get_class($classOrObject) : $classOrObject;
193+
194+
if (!isset(self::$reflectionCache[$class])) {
195+
self::$reflectionCache[$class] = new \ReflectionClass($class);
196+
}
197+
198+
$reflection = self::$reflectionCache[$class];
199+
200+
do {
201+
if ($read) {
202+
foreach (['get', 'is', 'has'] as $getterPrefix) {
203+
$getter = $getterPrefix.ucfirst($attribute);
204+
$reflectionMethod = $reflection->hasMethod($getter) ? $reflection->getMethod($getter) : null;
205+
if ($reflectionMethod && $this->isGetMethod($reflectionMethod)) {
206+
return true;
207+
}
208+
}
209+
} else {
210+
$setter = 'set'.ucfirst($attribute);
211+
$reflectionMethod = $reflection->hasMethod($setter) ? $reflection->getMethod($setter) : null;
212+
if ($reflectionMethod && $this->isSetMethod($reflectionMethod)) {
213+
return true;
214+
}
215+
216+
$constructor = $reflection->getConstructor();
217+
218+
if ($constructor && $constructor->isPublic()) {
219+
foreach ($constructor->getParameters() as $parameter) {
220+
if ($parameter->getName() === $attribute) {
221+
return true;
222+
}
223+
}
224+
}
225+
}
226+
} while ($reflection = $reflection->getParentClass());
227+
228+
return false;
229+
}
163230
}

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

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
1515
use Symfony\Component\PropertyAccess\PropertyAccess;
1616
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
17+
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
18+
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
1719
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
20+
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
1821
use Symfony\Component\Serializer\Exception\LogicException;
1922
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
2023
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
@@ -29,10 +32,11 @@
2932
class ObjectNormalizer extends AbstractObjectNormalizer
3033
{
3134
protected $propertyAccessor;
35+
protected $propertyInfoExtractor;
3236

3337
private $objectClassResolver;
3438

35-
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [])
39+
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [], PropertyInfoExtractorInterface $propertyInfoExtractor = null)
3640
{
3741
if (!class_exists(PropertyAccess::class)) {
3842
throw new LogicException('The ObjectNormalizer class requires the "PropertyAccess" component. Install "symfony/property-access" to use it.');
@@ -45,6 +49,8 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory
4549
$this->objectClassResolver = $objectClassResolver ?? function ($class) {
4650
return \is_object($class) ? \get_class($class) : $class;
4751
};
52+
53+
$this->propertyInfoExtractor = $propertyInfoExtractor ?: new ReflectionExtractor();
4854
}
4955

5056
/**
@@ -58,8 +64,10 @@ public function hasCacheableSupportsMethod(): bool
5864
/**
5965
* {@inheritdoc}
6066
*/
61-
protected function extractAttributes(object $object, string $format = null, array $context = [])
67+
protected function extractAttributes(object $object, string $format = null, array $context = [] /* , bool $read = true */)
6268
{
69+
$read = \func_get_args()[3] ?? true;
70+
6371
if (\stdClass::class === \get_class($object)) {
6472
return array_keys((array) $object);
6573
}
@@ -100,7 +108,7 @@ protected function extractAttributes(object $object, string $format = null, arra
100108
}
101109
}
102110

103-
if (null !== $attributeName && $this->isAllowedAttribute($object, $attributeName, $format, $context)) {
111+
if (null !== $attributeName && $this->isAllowedAttribute($object, $attributeName, $format, $context, $read)) {
104112
$attributes[$attributeName] = true;
105113
}
106114
}
@@ -111,7 +119,7 @@ protected function extractAttributes(object $object, string $format = null, arra
111119
continue;
112120
}
113121

114-
if ($reflProperty->isStatic() || !$this->isAllowedAttribute($object, $reflProperty->name, $format, $context)) {
122+
if ($reflProperty->isStatic() || !$this->isAllowedAttribute($object, $reflProperty->name, $format, $context, $read)) {
115123
continue;
116124
}
117125

@@ -174,4 +182,21 @@ protected function getAllowedAttributes($classOrObject, array $context, bool $at
174182

175183
return $allowedAttributes;
176184
}
185+
186+
protected function isAllowedAttribute($classOrObject, string $attribute, string $format = null, array $context = [] /* , bool $read = true */)
187+
{
188+
$read = \func_get_args()[4] ?? true;
189+
190+
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context, $read)) {
191+
return false;
192+
}
193+
$class = \is_object($classOrObject) ? \get_class($classOrObject) : $classOrObject;
194+
195+
if ($read) {
196+
return $this->propertyInfoExtractor->isReadable($class, $attribute);
197+
}
198+
199+
return $this->propertyInfoExtractor->isWritable($class, $attribute)
200+
|| null !== ($writeInfo = $this->propertyInfoExtractor->getWriteInfo($class, $attribute)) && PropertyWriteInfo::TYPE_NONE !== $writeInfo->getType();
201+
}
177202
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,11 @@ private function supports(string $class): bool
8282
/**
8383
* {@inheritdoc}
8484
*/
85-
protected function isAllowedAttribute($classOrObject, string $attribute, string $format = null, array $context = [])
85+
protected function isAllowedAttribute($classOrObject, string $attribute, string $format = null, array $context = [] /* , bool $read = true */)
8686
{
87-
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
87+
$read = \func_get_args()[4] ?? true;
88+
89+
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context, $read)) {
8890
return false;
8991
}
9092

@@ -103,14 +105,16 @@ protected function isAllowedAttribute($classOrObject, string $attribute, string
103105
/**
104106
* {@inheritdoc}
105107
*/
106-
protected function extractAttributes(object $object, string $format = null, array $context = [])
108+
protected function extractAttributes(object $object, string $format = null, array $context = [] /* , bool $read = true */)
107109
{
110+
$read = \func_get_args()[3] ?? true;
111+
108112
$reflectionObject = new \ReflectionObject($object);
109113
$attributes = [];
110114

111115
do {
112116
foreach ($reflectionObject->getProperties() as $property) {
113-
if (!$this->isAllowedAttribute($reflectionObject->getName(), $property->name, $format, $context)) {
117+
if (!$this->isAllowedAttribute($reflectionObject->getName(), $property->name, $format, $context, $read)) {
114118
continue;
115119
}
116120

0 commit comments

Comments
 (0)