bug #38387 [Validator] prevent hash collisions caused by reused object hashes (fancyweb, xabbuh)
This PR was merged into the 4.4 branch. Discussion ---------- [Validator] prevent hash collisions caused by reused object hashes | Q | A | ------------- | --- | Branch? | 3.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | Fix #36415 | License | MIT | Doc PR | Commits -------8dd1a6e545
prevent hash collisions caused by reused object hashes9645fa39ec
[Validator][RecursiveContextualValidator] Prevent validated hash collisions
This commit is contained in:
commit
c72f85333a
@ -25,7 +25,6 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
|||||||
class FormValidator extends ConstraintValidator
|
class FormValidator extends ConstraintValidator
|
||||||
{
|
{
|
||||||
private $resolvedGroups;
|
private $resolvedGroups;
|
||||||
private $fieldFormConstraints;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
@ -68,7 +67,6 @@ class FormValidator extends ConstraintValidator
|
|||||||
|
|
||||||
if ($hasChildren && $form->isRoot()) {
|
if ($hasChildren && $form->isRoot()) {
|
||||||
$this->resolvedGroups = new \SplObjectStorage();
|
$this->resolvedGroups = new \SplObjectStorage();
|
||||||
$this->fieldFormConstraints = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($groups instanceof GroupSequence) {
|
if ($groups instanceof GroupSequence) {
|
||||||
@ -93,7 +91,6 @@ class FormValidator extends ConstraintValidator
|
|||||||
$this->resolvedGroups[$field] = (array) $group;
|
$this->resolvedGroups[$field] = (array) $group;
|
||||||
$fieldFormConstraint = new Form();
|
$fieldFormConstraint = new Form();
|
||||||
$fieldFormConstraint->groups = $group;
|
$fieldFormConstraint->groups = $group;
|
||||||
$this->fieldFormConstraints[] = $fieldFormConstraint;
|
|
||||||
$this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath());
|
$this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath());
|
||||||
$validator->atPath(sprintf('children[%s]', $field->getName()))->validate($field, $fieldFormConstraint, $group);
|
$validator->atPath(sprintf('children[%s]', $field->getName()))->validate($field, $fieldFormConstraint, $group);
|
||||||
}
|
}
|
||||||
@ -139,10 +136,8 @@ class FormValidator extends ConstraintValidator
|
|||||||
foreach ($form->all() as $field) {
|
foreach ($form->all() as $field) {
|
||||||
if ($field->isSubmitted()) {
|
if ($field->isSubmitted()) {
|
||||||
$this->resolvedGroups[$field] = $groups;
|
$this->resolvedGroups[$field] = $groups;
|
||||||
$fieldFormConstraint = new Form();
|
|
||||||
$this->fieldFormConstraints[] = $fieldFormConstraint;
|
|
||||||
$this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath());
|
$this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath());
|
||||||
$validator->atPath(sprintf('children[%s]', $field->getName()))->validate($field, $fieldFormConstraint);
|
$validator->atPath(sprintf('children[%s]', $field->getName()))->validate($field, $formConstraint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,7 +145,6 @@ class FormValidator extends ConstraintValidator
|
|||||||
if ($hasChildren && $form->isRoot()) {
|
if ($hasChildren && $form->isRoot()) {
|
||||||
// destroy storage to avoid memory leaks
|
// destroy storage to avoid memory leaks
|
||||||
$this->resolvedGroups = new \SplObjectStorage();
|
$this->resolvedGroups = new \SplObjectStorage();
|
||||||
$this->fieldFormConstraints = [];
|
|
||||||
}
|
}
|
||||||
} elseif (!$form->isSynchronized()) {
|
} elseif (!$form->isSynchronized()) {
|
||||||
$childrenSynchronized = true;
|
$childrenSynchronized = true;
|
||||||
@ -159,11 +153,8 @@ class FormValidator extends ConstraintValidator
|
|||||||
foreach ($form as $child) {
|
foreach ($form as $child) {
|
||||||
if (!$child->isSynchronized()) {
|
if (!$child->isSynchronized()) {
|
||||||
$childrenSynchronized = false;
|
$childrenSynchronized = false;
|
||||||
|
|
||||||
$fieldFormConstraint = new Form();
|
|
||||||
$this->fieldFormConstraints[] = $fieldFormConstraint;
|
|
||||||
$this->context->setNode($this->context->getValue(), $child, $this->context->getMetadata(), $this->context->getPropertyPath());
|
$this->context->setNode($this->context->getValue(), $child, $this->context->getMetadata(), $this->context->getPropertyPath());
|
||||||
$validator->atPath(sprintf('children[%s]', $child->getName()))->validate($child, $fieldFormConstraint);
|
$validator->atPath(sprintf('children[%s]', $child->getName()))->validate($child, $formConstraint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"doctrine/collections": "~1.0",
|
"doctrine/collections": "~1.0",
|
||||||
"symfony/validator": "^3.4.44|^4.3.4|^5.0",
|
"symfony/validator": "^4.4.17|^5.1.9",
|
||||||
"symfony/dependency-injection": "^3.4|^4.0|^5.0",
|
"symfony/dependency-injection": "^3.4|^4.0|^5.0",
|
||||||
"symfony/expression-language": "^3.4|^4.0|^5.0",
|
"symfony/expression-language": "^3.4|^4.0|^5.0",
|
||||||
"symfony/config": "^3.4|^4.0|^5.0",
|
"symfony/config": "^3.4|^4.0|^5.0",
|
||||||
|
@ -129,6 +129,7 @@ class ExecutionContext implements ExecutionContextInterface
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
private $initializedObjects;
|
private $initializedObjects;
|
||||||
|
private $cachedObjectsRefs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new execution context.
|
* Creates a new execution context.
|
||||||
@ -153,6 +154,7 @@ class ExecutionContext implements ExecutionContextInterface
|
|||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
$this->translationDomain = $translationDomain;
|
$this->translationDomain = $translationDomain;
|
||||||
$this->violations = new ConstraintViolationList();
|
$this->violations = new ConstraintViolationList();
|
||||||
|
$this->cachedObjectsRefs = new \SplObjectStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -358,4 +360,20 @@ class ExecutionContext implements ExecutionContextInterface
|
|||||||
{
|
{
|
||||||
return isset($this->initializedObjects[$cacheKey]);
|
return isset($this->initializedObjects[$cacheKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @param object $object
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function generateCacheKey($object)
|
||||||
|
{
|
||||||
|
if (!isset($this->cachedObjectsRefs[$object])) {
|
||||||
|
$this->cachedObjectsRefs[$object] = spl_object_hash($object);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->cachedObjectsRefs[$object];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,14 @@ namespace Symfony\Component\Validator\Tests\Validator;
|
|||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\Translation\IdentityTranslator;
|
use Symfony\Component\Translation\IdentityTranslator;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
use Symfony\Component\Validator\Constraints\All;
|
use Symfony\Component\Validator\Constraints\All;
|
||||||
use Symfony\Component\Validator\Constraints\Callback;
|
use Symfony\Component\Validator\Constraints\Callback;
|
||||||
use Symfony\Component\Validator\Constraints\Collection;
|
use Symfony\Component\Validator\Constraints\Collection;
|
||||||
use Symfony\Component\Validator\Constraints\Expression;
|
use Symfony\Component\Validator\Constraints\Expression;
|
||||||
use Symfony\Component\Validator\Constraints\GroupSequence;
|
use Symfony\Component\Validator\Constraints\GroupSequence;
|
||||||
|
use Symfony\Component\Validator\Constraints\IsFalse;
|
||||||
|
use Symfony\Component\Validator\Constraints\IsNull;
|
||||||
use Symfony\Component\Validator\Constraints\IsTrue;
|
use Symfony\Component\Validator\Constraints\IsTrue;
|
||||||
use Symfony\Component\Validator\Constraints\Length;
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
@ -26,6 +29,7 @@ use Symfony\Component\Validator\Constraints\Optional;
|
|||||||
use Symfony\Component\Validator\Constraints\Required;
|
use Symfony\Component\Validator\Constraints\Required;
|
||||||
use Symfony\Component\Validator\Constraints\Traverse;
|
use Symfony\Component\Validator\Constraints\Traverse;
|
||||||
use Symfony\Component\Validator\Constraints\Valid;
|
use Symfony\Component\Validator\Constraints\Valid;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
use Symfony\Component\Validator\ConstraintValidatorFactory;
|
use Symfony\Component\Validator\ConstraintValidatorFactory;
|
||||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||||
use Symfony\Component\Validator\Context\ExecutionContextFactory;
|
use Symfony\Component\Validator\Context\ExecutionContextFactory;
|
||||||
@ -2135,4 +2139,66 @@ class RecursiveValidatorTest extends TestCase
|
|||||||
|
|
||||||
$this->assertCount(0, $violations);
|
$this->assertCount(0, $violations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testValidatedConstraintsHashesDoNotCollide()
|
||||||
|
{
|
||||||
|
$metadata = new ClassMetadata(Entity::class);
|
||||||
|
$metadata->addPropertyConstraint('initialized', new NotNull(['groups' => 'should_pass']));
|
||||||
|
$metadata->addPropertyConstraint('initialized', new IsNull(['groups' => 'should_fail']));
|
||||||
|
|
||||||
|
$this->metadataFactory->addMetadata($metadata);
|
||||||
|
|
||||||
|
$entity = new Entity();
|
||||||
|
$entity->data = new \stdClass();
|
||||||
|
|
||||||
|
$this->assertCount(2, $this->validator->validate($entity, new TestConstraintHashesDoNotCollide()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidatedConstraintsHashesDoNotCollideWithSameConstraintValidatingDifferentProperties()
|
||||||
|
{
|
||||||
|
$value = new \stdClass();
|
||||||
|
|
||||||
|
$entity = new Entity();
|
||||||
|
$entity->firstName = $value;
|
||||||
|
$entity->setLastName($value);
|
||||||
|
|
||||||
|
$validator = $this->validator->startContext($entity);
|
||||||
|
|
||||||
|
$constraint = new IsNull();
|
||||||
|
$validator->atPath('firstName')
|
||||||
|
->validate($entity->firstName, $constraint);
|
||||||
|
$validator->atPath('lastName')
|
||||||
|
->validate($entity->getLastName(), $constraint);
|
||||||
|
|
||||||
|
$this->assertCount(2, $validator->getViolations());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TestConstraintHashesDoNotCollide extends Constraint
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TestConstraintHashesDoNotCollideValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function validate($value, Constraint $constraint)
|
||||||
|
{
|
||||||
|
if (!$value instanceof Entity) {
|
||||||
|
throw new \LogicException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->context->getValidator()
|
||||||
|
->inContext($this->context)
|
||||||
|
->atPath('data')
|
||||||
|
->validate($value, new NotNull())
|
||||||
|
->validate($value, new NotNull())
|
||||||
|
->validate($value, new IsFalse());
|
||||||
|
|
||||||
|
$this->context->getValidator()
|
||||||
|
->inContext($this->context)
|
||||||
|
->validate($value, null, new GroupSequence(['should_pass']))
|
||||||
|
->validate($value, null, new GroupSequence(['should_fail']));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ class RecursiveContextualValidator implements ContextualValidatorInterface
|
|||||||
$this->validateGenericNode(
|
$this->validateGenericNode(
|
||||||
$value,
|
$value,
|
||||||
$previousObject,
|
$previousObject,
|
||||||
\is_object($value) ? spl_object_hash($value) : null,
|
\is_object($value) ? $this->generateCacheKey($value) : null,
|
||||||
$metadata,
|
$metadata,
|
||||||
$this->defaultPropertyPath,
|
$this->defaultPropertyPath,
|
||||||
$groups,
|
$groups,
|
||||||
@ -176,7 +176,7 @@ class RecursiveContextualValidator implements ContextualValidatorInterface
|
|||||||
|
|
||||||
$propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
|
$propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
|
||||||
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
|
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
|
||||||
$cacheKey = spl_object_hash($object);
|
$cacheKey = $this->generateCacheKey($object);
|
||||||
$propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
|
$propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
|
||||||
|
|
||||||
$previousValue = $this->context->getValue();
|
$previousValue = $this->context->getValue();
|
||||||
@ -224,7 +224,7 @@ class RecursiveContextualValidator implements ContextualValidatorInterface
|
|||||||
if (\is_object($objectOrClass)) {
|
if (\is_object($objectOrClass)) {
|
||||||
$object = $objectOrClass;
|
$object = $objectOrClass;
|
||||||
$class = \get_class($object);
|
$class = \get_class($object);
|
||||||
$cacheKey = spl_object_hash($objectOrClass);
|
$cacheKey = $this->generateCacheKey($objectOrClass);
|
||||||
$propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
|
$propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
|
||||||
} else {
|
} else {
|
||||||
// $objectOrClass contains a class name
|
// $objectOrClass contains a class name
|
||||||
@ -313,7 +313,7 @@ class RecursiveContextualValidator implements ContextualValidatorInterface
|
|||||||
|
|
||||||
$this->validateClassNode(
|
$this->validateClassNode(
|
||||||
$object,
|
$object,
|
||||||
spl_object_hash($object),
|
$this->generateCacheKey($object),
|
||||||
$classMetadata,
|
$classMetadata,
|
||||||
$propertyPath,
|
$propertyPath,
|
||||||
$groups,
|
$groups,
|
||||||
@ -429,7 +429,7 @@ class RecursiveContextualValidator implements ContextualValidatorInterface
|
|||||||
$defaultOverridden = false;
|
$defaultOverridden = false;
|
||||||
|
|
||||||
// Use the object hash for group sequences
|
// Use the object hash for group sequences
|
||||||
$groupHash = \is_object($group) ? spl_object_hash($group) : $group;
|
$groupHash = \is_object($group) ? $this->generateCacheKey($group, true) : $group;
|
||||||
|
|
||||||
if ($context->isGroupValidated($cacheKey, $groupHash)) {
|
if ($context->isGroupValidated($cacheKey, $groupHash)) {
|
||||||
// Skip this group when validating the properties and when
|
// Skip this group when validating the properties and when
|
||||||
@ -740,7 +740,7 @@ class RecursiveContextualValidator implements ContextualValidatorInterface
|
|||||||
// Prevent duplicate validation of constraints, in the case
|
// Prevent duplicate validation of constraints, in the case
|
||||||
// that constraints belong to multiple validated groups
|
// that constraints belong to multiple validated groups
|
||||||
if (null !== $cacheKey) {
|
if (null !== $cacheKey) {
|
||||||
$constraintHash = spl_object_hash($constraint);
|
$constraintHash = $this->generateCacheKey($constraint, true);
|
||||||
// instanceof Valid: In case of using a Valid constraint with many groups
|
// instanceof Valid: In case of using a Valid constraint with many groups
|
||||||
// it makes a reference object get validated by each group
|
// it makes a reference object get validated by each group
|
||||||
if ($constraint instanceof Composite || $constraint instanceof Valid) {
|
if ($constraint instanceof Composite || $constraint instanceof Valid) {
|
||||||
@ -772,4 +772,22 @@ class RecursiveContextualValidator implements ContextualValidatorInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param object $object
|
||||||
|
*/
|
||||||
|
private function generateCacheKey($object, bool $dependsOnPropertyPath = false): string
|
||||||
|
{
|
||||||
|
if ($this->context instanceof ExecutionContext) {
|
||||||
|
$cacheKey = $this->context->generateCacheKey($object);
|
||||||
|
} else {
|
||||||
|
$cacheKey = spl_object_hash($object);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dependsOnPropertyPath) {
|
||||||
|
$cacheKey .= $this->context->getPropertyPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cacheKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user