From 6359b3ea5aacf85399285c522c6d787a218c897c Mon Sep 17 00:00:00 2001 From: xjm Date: Tue, 14 Sep 2021 17:08:09 -0500 Subject: [PATCH] SA-CORE-2021-007 by samuel.mortenson, Wim Leers, greggles, xjm, larowlan, vijaycs85, Heine, effulgentsia, phenaproxima, mcdruid, nod_ --- .../quickedit/js/models/EntityModel.es6.js | 3 ++ modules/quickedit/js/models/EntityModel.js | 2 ++ modules/quickedit/quickedit.module | 1 + modules/quickedit/src/QuickEditController.php | 30 +++++++++++++++++++ 4 files changed, 36 insertions(+) diff --git a/modules/quickedit/js/models/EntityModel.es6.js b/modules/quickedit/js/models/EntityModel.es6.js index 2984425318b..419695b77c4 100644 --- a/modules/quickedit/js/models/EntityModel.es6.js +++ b/modules/quickedit/js/models/EntityModel.es6.js @@ -526,6 +526,9 @@ options.success.call(entityModel); } }; + entitySaverAjax.options.headers = entitySaverAjax.options.headers || {}; + entitySaverAjax.options.headers['X-Drupal-Quickedit-CSRF-Token'] = + drupalSettings.quickedit.csrf_token; // Trigger the AJAX request, which will will return the // quickeditEntitySaved AJAX command to which we then react. entitySaverAjax.execute(); diff --git a/modules/quickedit/js/models/EntityModel.js b/modules/quickedit/js/models/EntityModel.js index 71da827ace7..2d6749d7ac8 100644 --- a/modules/quickedit/js/models/EntityModel.js +++ b/modules/quickedit/js/models/EntityModel.js @@ -243,6 +243,8 @@ options.success.call(entityModel); } }; + entitySaverAjax.options.headers = entitySaverAjax.options.headers || {}; + entitySaverAjax.options.headers['X-Drupal-Quickedit-CSRF-Token'] = drupalSettings.quickedit.csrf_token; entitySaverAjax.execute(); }, diff --git a/modules/quickedit/quickedit.module b/modules/quickedit/quickedit.module index 351ac6987a6..8e216542469 100644 --- a/modules/quickedit/quickedit.module +++ b/modules/quickedit/quickedit.module @@ -53,6 +53,7 @@ function quickedit_page_attachments(array &$page) { return; } + $page['#attached']['drupalSettings']['quickedit']['csrf_token'] = \Drupal::csrfToken()->get('X-Drupal-Quickedit-CSRF-Token'); $page['#attached']['library'][] = 'quickedit/quickedit'; } diff --git a/modules/quickedit/src/QuickEditController.php b/modules/quickedit/src/QuickEditController.php index c2dac0f7e0a..7796c6fc991 100644 --- a/modules/quickedit/src/QuickEditController.php +++ b/modules/quickedit/src/QuickEditController.php @@ -6,10 +6,12 @@ use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Form\FormState; use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\Core\TempStore\PrivateTempStoreFactory; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Entity\EntityInterface; @@ -165,6 +167,32 @@ public function metadata(Request $request) { return new JsonResponse($metadata); } + /** + * Throws an AccessDeniedHttpException if the request fails CSRF validation. + * + * This is used instead of \Drupal\Core\Access\CsrfAccessCheck, in order to + * allow access for anonymous users. + * + * @todo Refactor this to an access checker. + */ + private static function checkCsrf(Request $request, AccountInterface $account) { + $header = 'X-Drupal-Quickedit-CSRF-Token'; + + if (!$request->headers->has($header)) { + throw new AccessDeniedHttpException(); + } + if ($account->isAnonymous()) { + // For anonymous users, just the presence of the custom header is + // sufficient protection. + return; + } + // For authenticated users, validate the token value. + $token = $request->headers->get($header); + if (!\Drupal::csrfToken()->validate($token, $header)) { + throw new AccessDeniedHttpException(); + } + } + /** * Returns AJAX commands to load in-place editors' attachments. * @@ -315,6 +343,8 @@ protected function renderField(EntityInterface $entity, $field_name, $langcode, * The Ajax response. */ public function entitySave(EntityInterface $entity) { + self::checkCsrf(\Drupal::request(), \Drupal::currentUser()); + // Take the entity from PrivateTempStore and save in entity storage. // fieldForm() ensures that the PrivateTempStore copy exists ahead. $tempstore = $this->tempStoreFactory->get('quickedit');