Skip to content

Commit fb8a95a

Browse files
committed
prevent hash collisions caused by reused object hashes
1 parent 0026bd4 commit fb8a95a

File tree

3 files changed

+61
-6
lines changed

3 files changed

+61
-6
lines changed

Context/ExecutionContext.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ class ExecutionContext implements ExecutionContextInterface
129129
* @var array
130130
*/
131131
private $initializedObjects;
132+
private $cachedObjectsRefs;
132133

133134
/**
134135
* Creates a new execution context.
@@ -153,6 +154,7 @@ public function __construct(ValidatorInterface $validator, $root, $translator, s
153154
$this->translator = $translator;
154155
$this->translationDomain = $translationDomain;
155156
$this->violations = new ConstraintViolationList();
157+
$this->cachedObjectsRefs = new \SplObjectStorage();
156158
}
157159

158160
/**
@@ -358,4 +360,20 @@ public function isObjectInitialized($cacheKey): bool
358360
{
359361
return isset($this->initializedObjects[$cacheKey]);
360362
}
363+
364+
/**
365+
* @internal
366+
*
367+
* @param object $object
368+
*
369+
* @return string
370+
*/
371+
public function generateCacheKey($object)
372+
{
373+
if (!isset($this->cachedObjectsRefs[$object])) {
374+
$this->cachedObjectsRefs[$object] = spl_object_hash($object);
375+
}
376+
377+
return $this->cachedObjectsRefs[$object];
378+
}
361379
}

Tests/Validator/RecursiveValidatorTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2153,6 +2153,25 @@ public function testValidatedConstraintsHashesDoNotCollide()
21532153

21542154
$this->assertCount(2, $this->validator->validate($entity, new TestConstraintHashesDoNotCollide()));
21552155
}
2156+
2157+
public function testValidatedConstraintsHashesDoNotCollideWithSameConstraintValidatingDifferentProperties()
2158+
{
2159+
$value = new \stdClass();
2160+
2161+
$entity = new Entity();
2162+
$entity->firstName = $value;
2163+
$entity->setLastName($value);
2164+
2165+
$validator = $this->validator->startContext($entity);
2166+
2167+
$constraint = new IsNull();
2168+
$validator->atPath('firstName')
2169+
->validate($entity->firstName, $constraint);
2170+
$validator->atPath('lastName')
2171+
->validate($entity->getLastName(), $constraint);
2172+
2173+
$this->assertCount(2, $validator->getViolations());
2174+
}
21562175
}
21572176

21582177
final class TestConstraintHashesDoNotCollide extends Constraint

Validator/RecursiveContextualValidator.php

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public function validate($value, $constraints = null, $groups = null)
108108
$this->validateGenericNode(
109109
$value,
110110
$previousObject,
111-
\is_object($value) ? spl_object_hash($value) : null,
111+
\is_object($value) ? $this->generateCacheKey($value) : null,
112112
$metadata,
113113
$this->defaultPropertyPath,
114114
$groups,
@@ -176,7 +176,7 @@ public function validateProperty($object, $propertyName, $groups = null)
176176

177177
$propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
178178
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
179-
$cacheKey = spl_object_hash($object);
179+
$cacheKey = $this->generateCacheKey($object);
180180
$propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
181181

182182
$previousValue = $this->context->getValue();
@@ -224,7 +224,7 @@ public function validatePropertyValue($objectOrClass, $propertyName, $value, $gr
224224
if (\is_object($objectOrClass)) {
225225
$object = $objectOrClass;
226226
$class = \get_class($object);
227-
$cacheKey = spl_object_hash($objectOrClass);
227+
$cacheKey = $this->generateCacheKey($objectOrClass);
228228
$propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
229229
} else {
230230
// $objectOrClass contains a class name
@@ -313,7 +313,7 @@ private function validateObject($object, string $propertyPath, array $groups, in
313313

314314
$this->validateClassNode(
315315
$object,
316-
spl_object_hash($object),
316+
$this->generateCacheKey($object),
317317
$classMetadata,
318318
$propertyPath,
319319
$groups,
@@ -429,7 +429,7 @@ private function validateClassNode($object, ?string $cacheKey, ClassMetadataInte
429429
$defaultOverridden = false;
430430

431431
// Use the object hash for group sequences
432-
$groupHash = \is_object($group) ? spl_object_hash($group) : $group;
432+
$groupHash = \is_object($group) ? $this->generateCacheKey($group, true) : $group;
433433

434434
if ($context->isGroupValidated($cacheKey, $groupHash)) {
435435
// Skip this group when validating the properties and when
@@ -740,7 +740,7 @@ private function validateInGroup($value, ?string $cacheKey, MetadataInterface $m
740740
// Prevent duplicate validation of constraints, in the case
741741
// that constraints belong to multiple validated groups
742742
if (null !== $cacheKey) {
743-
$constraintHash = spl_object_hash($constraint);
743+
$constraintHash = $this->generateCacheKey($constraint, true);
744744
// instanceof Valid: In case of using a Valid constraint with many groups
745745
// it makes a reference object get validated by each group
746746
if ($constraint instanceof Composite || $constraint instanceof Valid) {
@@ -772,4 +772,22 @@ private function validateInGroup($value, ?string $cacheKey, MetadataInterface $m
772772
}
773773
}
774774
}
775+
776+
/**
777+
* @param object $object
778+
*/
779+
private function generateCacheKey($object, bool $dependsOnPropertyPath = false): string
780+
{
781+
if ($this->context instanceof ExecutionContext) {
782+
$cacheKey = $this->context->generateCacheKey($object);
783+
} else {
784+
$cacheKey = spl_object_hash($object);
785+
}
786+
787+
if ($dependsOnPropertyPath) {
788+
$cacheKey .= $this->context->getPropertyPath();
789+
}
790+
791+
return $cacheKey;
792+
}
775793
}

0 commit comments

Comments
 (0)