Commit 4a576b0b authored by alexpott's avatar alexpott

Issue #2490420 by amateescu, larowlan, Berdir, dpi: EntityAutocomplete element...

Issue #2490420 by amateescu, larowlan, Berdir, dpi: EntityAutocomplete element settings allows sql injection and for arbitrary user-supplied data to be passed into unserialize()
parent 684602ea
......@@ -7,10 +7,12 @@
namespace Drupal\Core\Entity\Element;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\Textfield;
use Drupal\Core\Site\Settings;
use Drupal\user\EntityOwnerInterface;
/**
......@@ -112,11 +114,22 @@ public static function processEntityAutocomplete(array &$element, FormStateInter
$element['#autocreate']['uid'] = isset($element['#autocreate']['uid']) ? $element['#autocreate']['uid'] : \Drupal::currentUser()->id();
}
// Store the selection settings in the key/value store and pass a hashed key
// in the route parameters.
$selection_settings = isset($element['#selection_settings']) ? $element['#selection_settings'] : [];
$data = serialize($selection_settings) . $element['#target_type'] . $element['#selection_handler'];
$selection_settings_key = Crypt::hmacBase64($data, Settings::getHashSalt());
$key_value_storage = \Drupal::keyValue('entity_autocomplete');
if (!$key_value_storage->has($selection_settings_key)) {
$key_value_storage->set($selection_settings_key, $selection_settings);
}
$element['#autocomplete_route_name'] = 'system.entity_autocomplete';
$element['#autocomplete_route_parameters'] = array(
'target_type' => $element['#target_type'],
'selection_handler' => $element['#selection_handler'],
'selection_settings' => $element['#selection_settings'] ? base64_encode(serialize($element['#selection_settings'])) : '',
'selection_settings_key' => $selection_settings_key,
);
return $element;
......
......@@ -50,8 +50,7 @@ protected function setUp() {
$this->installEntitySchema('entity_test');
$this->installEntitySchema('user');
$this->installSchema('system', array('sequences'));
$this->installSchema('system', array('router'));
$this->installSchema('system', ['router', 'sequences', 'key_value']);
// Set default storage backend and configure the theme system.
$this->installConfig(array('field', 'system'));
......
......@@ -7,13 +7,17 @@
namespace Drupal\system\Controller;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Tags;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityAutocompleteMatcher;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Site\Settings;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Defines a route controller for entity autocomplete form elements.
......@@ -27,14 +31,24 @@ class EntityAutocompleteController extends ControllerBase {
*/
protected $matcher;
/**
* The key value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $keyValue;
/**
* Constructs a EntityAutocompleteController object.
*
* @param \Drupal\Core\Entity\EntityAutocompleteMatcher $matcher
* The autocomplete matcher for entity references.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value
* The key value factory.
*/
public function __construct(EntityAutocompleteMatcher $matcher) {
public function __construct(EntityAutocompleteMatcher $matcher, KeyValueStoreInterface $key_value) {
$this->matcher = $matcher;
$this->keyValue = $key_value;
}
/**
......@@ -42,7 +56,8 @@ public function __construct(EntityAutocompleteMatcher $matcher) {
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.autocomplete_matcher')
$container->get('entity.autocomplete_matcher'),
$container->get('keyvalue')->get('entity_autocomplete')
);
}
......@@ -55,21 +70,40 @@ public static function create(ContainerInterface $container) {
* The ID of the target entity type.
* @param string $selection_handler
* The plugin ID of the entity reference selection handler.
* @param string $selection_settings
* The settings that will be passed to the selection handler.
* @param string $selection_settings_key
* The hashed key of the key/value entry that holds the selection handler
* settings.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The matched entity labels as a JSON response.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Thrown if the selection settings key is not found in the key/value store
* or if it does not match the stored data.
*/
public function handleAutocomplete(Request $request, $target_type, $selection_handler, $selection_settings = '') {
public function handleAutocomplete(Request $request, $target_type, $selection_handler, $selection_settings_key) {
$matches = array();
// Get the typed string from the URL, if it exists.
if ($input = $request->query->get('q')) {
$typed_string = Tags::explode($input);
$typed_string = Unicode::strtolower(array_pop($typed_string));
// Selection settings are passed in as an encoded serialized array.
$selection_settings = $selection_settings ? unserialize(base64_decode($selection_settings)) : array();
// Selection settings are passed in as a hashed key of a serialized array
// stored in the key/value store.
$selection_settings = $this->keyValue->get($selection_settings_key, FALSE);
if ($selection_settings !== FALSE) {
$selection_settings_hash = Crypt::hmacBase64(serialize($selection_settings) . $target_type . $selection_handler, Settings::getHashSalt());
if ($selection_settings_hash !== $selection_settings_key) {
// Disallow access when the selection settings hash does not match the
// passed-in key.
throw new AccessDeniedHttpException('Invalid selection settings key.');
}
}
else {
// Disallow access when the selection settings key is not found in the
// key/value store.
throw new AccessDeniedHttpException();
}
$matches = $this->matcher->getMatches($target_type, $selection_handler, $selection_settings, $typed_string);
}
......
......@@ -8,10 +8,13 @@
namespace Drupal\system\Tests\Entity;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Site\Settings;
use Drupal\system\Controller\EntityAutocompleteController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Tests the autocomplete functionality.
......@@ -34,6 +37,14 @@ class EntityAutocompleteTest extends EntityUnitTestBase {
*/
protected $bundle = 'entity_test';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', ['key_value']);
}
/**
* Tests autocompletion edge cases with slashes in the names.
*/
......@@ -86,6 +97,47 @@ function testEntityReferenceAutocompletion() {
$this->assertIdentical(reset($data), $target, 'Autocomplete returns an entity label containing a comma and a slash.');
}
/**
* Tests that missing or invalid selection setting key are handled correctly.
*/
public function testSelectionSettingsHandling() {
$entity_reference_controller = EntityAutocompleteController::create($this->container);
$request = Request::create('entity_reference_autocomplete/' . $this->entityType . '/default');
$request->query->set('q', $this->randomString());
try {
// Pass an invalid selection settings key (i.e. one that does not exist
// in the key/value store).
$selection_settings_key = $this->randomString();
$entity_reference_controller->handleAutocomplete($request, $this->entityType, 'default', $selection_settings_key);
$this->fail('Non-existent selection settings key throws an exception.');
}
catch (AccessDeniedHttpException $e) {
$this->pass('Non-existent selection settings key throws an exception.');
}
try {
// Generate a valid hash key but store a modified settings array.
$selection_settings = [];
$selection_settings_key = Crypt::hmacBase64(serialize($selection_settings) . $this->entityType . 'default', Settings::getHashSalt());
$selection_settings[$this->randomMachineName()] = $this->randomString();
\Drupal::keyValue('entity_autocomplete')->set($selection_settings_key, $selection_settings);
$entity_reference_controller->handleAutocomplete($request, $this->entityType, 'default', $selection_settings_key);
}
catch (AccessDeniedHttpException $e) {
if ($e->getMessage() == 'Invalid selection settings key.') {
$this->pass('Invalid selection settings key throws an exception.');
}
else {
$this->fail('Invalid selection settings key throws an exception.');
}
}
}
/**
* Returns the result of an Entity reference autocomplete request.
*
......@@ -99,8 +151,12 @@ protected function getAutocompleteResult($input) {
$request = Request::create('entity_reference_autocomplete/' . $this->entityType . '/default');
$request->query->set('q', $input);
$selection_settings = [];
$selection_settings_key = Crypt::hmacBase64(serialize($selection_settings) . $this->entityType . 'default', Settings::getHashSalt());
\Drupal::keyValue('entity_autocomplete')->set($selection_settings_key, $selection_settings);
$entity_reference_controller = EntityAutocompleteController::create($this->container);
$result = $entity_reference_controller->handleAutocomplete($request, $this->entityType, 'default')->getContent();
$result = $entity_reference_controller->handleAutocomplete($request, $this->entityType, 'default', $selection_settings_key)->getContent();
return Json::decode($result);
}
......
......@@ -26,7 +26,7 @@ class FieldWidgetConstraintValidatorTest extends KernelTestBase {
protected function setUp() {
parent::setUp();
$this->installSchema('system', 'router');
$this->installSchema('system', ['router', 'key_value']);
$this->container->get('router.builder')->rebuild();
$this->installEntitySchema('user');
......
......@@ -476,9 +476,8 @@ system.admin_content:
_permission: 'access administration pages'
system.entity_autocomplete:
path: '/entity_reference_autocomplete/{target_type}/{selection_handler}/{selection_settings}'
path: '/entity_reference_autocomplete/{target_type}/{selection_handler}/{selection_settings_key}'
defaults:
_controller: '\Drupal\system\Controller\EntityAutocompleteController::handleAutocomplete'
selection_settings: ''
requirements:
_access: 'TRUE'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment