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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy