From 04f8d1d7d0e6e8408b46bfa72cceea56eccbcbf0 Mon Sep 17 00:00:00 2001 From: xjm Date: Tue, 14 Sep 2021 17:07:36 -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 433e1d5216c..a8838bdf13e 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 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 b8d0054859c..19cab8cc012 100644 --- a/modules/quickedit/js/models/EntityModel.js +++ b/modules/quickedit/js/models/EntityModel.js @@ -235,6 +235,8 @@ } }; + entitySaverAjax.options.headers = entitySaverAjax.options.headers || {}; + entitySaverAjax.options.headers['X-Drupal-Quickedit-CSRF-Token'] = drupalSettings.quickedit.csrf_token; entitySaverAjax.execute(); }, validate: function validate(attrs, options) { diff --git a/modules/quickedit/quickedit.module b/modules/quickedit/quickedit.module index b3b8eef894a..07a759d3a39 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 bc81769cdbf..455b7bfa6ca 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; @@ -157,6 +159,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. * @@ -307,6 +335,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');