VYPR
Moderate severityNVD Advisory· Published Feb 11, 2022· Updated Aug 4, 2024

CVE-2020-13674

CVE-2020-13674

Description

The QuickEdit module does not properly validate access to routes, which could allow cross-site request forgery under some circumstances and lead to possible data integrity issues. Sites are only affected if the QuickEdit module (which comes with the Standard profile) is installed. Removing the "access in-place editing" permission from untrusted users will not fully mitigate the vulnerability.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

QuickEdit module in Drupal Core fails to validate route access, enabling CSRF attacks that can corrupt data integrity.

Vulnerability

The QuickEdit module in Drupal Core (shipped with Standard profile) does not properly validate access to its AJAX routes. This allows cross-site request forgery (CSRF) under certain conditions. Affected versions: Drupal Core versions prior to the fix (commit 801910fcdfc14ee6120051089a2129e455186ad8 and 20cd85db8198c63101bd050ea973b13f2f3edef6). The module must be installed. Removing the "access in-place editing" permission does not fully mitigate [2].

Exploitation

An attacker can craft a malicious request that, if a logged-in user with the "access in-place editing" permission visits a crafted page, triggers an unauthorized AJAX call to QuickEdit endpoints. The attacker does not need authentication but relies on the victim's session. The fix adds a CSRF token header (X-Drupal-Quickedit-CSRF-Token) to AJAX requests and server-side validation [3][4].

Impact

Successful exploitation leads to data integrity issues: an attacker could modify content or settings via the QuickEdit interface without proper authorization. The impact is limited to integrity; confidentiality and availability are not directly affected.

Mitigation

The vulnerability is fixed in Drupal Core versions that include commits 801910fcdfc14ee6120051089a2129e455186ad8 and 20cd85db8198c63101bd050ea973b13f2f3edef6 (part of SA-CORE-2021-007). Users should update to the latest patched release. No workaround fully mitigates the issue; removing the permission is insufficient [2].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
drupal/corePackagist
>= 8.0.0, < 8.9.198.9.19
drupal/corePackagist
>= 9.1.0, < 9.1.139.1.13
drupal/corePackagist
>= 9.2.0, < 9.2.69.2.6

Affected products

3

Patches

3
6359b3ea5aac

SA-CORE-2021-007 by samuel.mortenson, Wim Leers, greggles, xjm, larowlan, vijaycs85, Heine, effulgentsia, phenaproxima, mcdruid, nod_

https://github.com/drupal/corexjmSep 14, 2021via ghsa
4 files changed · +36 0
  • modules/quickedit/js/models/EntityModel.es6.js+3 0 modified
    @@ -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();
    
  • modules/quickedit/js/models/EntityModel.js+2 0 modified
    @@ -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();
         },
    
  • modules/quickedit/quickedit.module+1 0 modified
    @@ -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';
     }
     
    
  • modules/quickedit/src/QuickEditController.php+30 0 modified
    @@ -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');
    
801910fcdfc1

SA-CORE-2021-007 by samuel.mortenson, Wim Leers, greggles, xjm, larowlan, vijaycs85, Heine, effulgentsia, phenaproxima, mcdruid, nod_

https://github.com/drupal/corexjmSep 14, 2021via ghsa
4 files changed · +36 0
  • modules/quickedit/js/models/EntityModel.es6.js+3 0 modified
    @@ -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();
    
  • modules/quickedit/js/models/EntityModel.js+2 0 modified
    @@ -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) {
    
  • modules/quickedit/quickedit.module+1 0 modified
    @@ -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';
     }
     
    
  • modules/quickedit/src/QuickEditController.php+30 0 modified
    @@ -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');
    
20cd85db8198

SA-CORE-2021-007 by samuel.mortenson, Wim Leers, greggles, xjm, larowlan, vijaycs85, Heine, effulgentsia, phenaproxima, mcdruid, nod_

https://github.com/drupal/corexjmSep 14, 2021via ghsa
4 files changed · +36 0
  • modules/quickedit/js/models/EntityModel.es6.js+3 0 modified
    @@ -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();
    
  • modules/quickedit/js/models/EntityModel.js+2 0 modified
    @@ -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) {
    
  • modules/quickedit/quickedit.module+1 0 modified
    @@ -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';
     }
     
    
  • modules/quickedit/src/QuickEditController.php+30 0 modified
    @@ -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');
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.