Skip to content

Commit

Permalink
Fixing autocomplete security bug by using the query_builder
Browse files Browse the repository at this point in the history
  • Loading branch information
weaverryan committed Sep 11, 2023
1 parent d562b80 commit fabcb2e
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 14 deletions.
27 changes: 14 additions & 13 deletions src/Form/AutocompleteEntityTypeSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@

namespace Symfony\UX\Autocomplete\Form;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\Utility\PersisterHelper;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
Expand Down Expand Up @@ -54,13 +52,15 @@ public function preSubmit(FormEvent $event)
$form = $event->getForm();
$options = $form->get('autocomplete')->getConfig()->getOptions();

/** @var EntityManagerInterface $em */
$em = $options['em'];
$repository = $em->getRepository($options['class']);
$queryBuilder = $options['query_builder'] ?: $repository->createQueryBuilder('o');
$rootAlias = $queryBuilder->getRootAliases()[0];

if (!isset($data['autocomplete']) || '' === $data['autocomplete']) {
$options['choices'] = [];
} else {
/** @var EntityManagerInterface $em */
$em = $options['em'];
$repository = $em->getRepository($options['class']);

$idField = $options['id_reader']->getIdField();
$idType = PersisterHelper::getTypeOfField($idField, $em->getClassMetadata($options['class']), $em)[0];

Expand All @@ -69,22 +69,23 @@ public function preSubmit(FormEvent $event)
$idx = 0;

foreach ($data['autocomplete'] as $id) {
$params[":id_$idx"] = new Parameter("id_$idx", $id, $idType);
$params[":id_$idx"] = [$id, $idType];
++$idx;
}

$queryBuilder = $repository->createQueryBuilder('o');

if ($params) {
$queryBuilder
->where(sprintf("o.$idField IN (%s)", implode(', ', array_keys($params))))
->setParameters(new ArrayCollection($params));
->andWhere(sprintf("$rootAlias.$idField IN (%s)", implode(', ', array_keys($params))))
;
foreach ($params as $key => $param) {
$queryBuilder->setParameter($key, $param[0], $param[1]);
}
}

$options['choices'] = $queryBuilder->getQuery()->getResult();
} else {
$options['choices'] = $repository->createQueryBuilder('o')
->where("o.$idField = :id")
$options['choices'] = $queryBuilder
->andWhere("$rootAlias.$idField = :id")
->setParameter('id', $data['autocomplete'], $idType)
->getQuery()
->getResult();
Expand Down
15 changes: 14 additions & 1 deletion tests/Functional/AutocompleteFormRenderingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,34 @@ public function testCategoryFieldSubmitsCorrectly()
$firstCat = CategoryFactory::createOne(['name' => 'First cat']);
CategoryFactory::createOne(['name' => 'in space']);
CategoryFactory::createOne(['name' => 'ate pizza']);
$fooCat = CategoryFactory::createOne(['name' => 'I contain "foo" which CategoryAutocompleteType uses its query_builder option.']);

$this->browser()
->throwExceptions()
->get('/test-form')
// the field renders empty (but the placeholder is there)
->assertElementCount('#product_category_autocomplete option', 1)
->assertNotContains('First cat')

->post('/test-form', [
'body' => [
'product' => ['category' => ['autocomplete' => $firstCat->getId()]],
],
])
// the option does NOT match something returned by query_builder
// so ONLY the placeholder shows up
->assertElementCount('#product_category_autocomplete option', 1)
->assertNotContains('First cat')

->assertNotContains('First cat')
->post('/test-form', [
'body' => [
'product' => ['category' => ['autocomplete' => $fooCat->getId()]],
],
])
// the one option + placeholder now shows up
->assertElementCount('#product_category_autocomplete option', 2)
->assertContains('First cat')
->assertContains('which CategoryAutocompleteType uses')
;
}

Expand Down

0 comments on commit fabcb2e

Please sign in to comment.