Verified Commit 2ef86a51 authored by godotislate's avatar godotislate
Browse files

fix: #3600777 Redirects can be used to bypass AJAX trusted URLs

By: prudloff
By: kieran.cott
By: smustgrave
By: godotislate
(cherry picked from commit 2aea80d1)
parent 04fa987a
Loading
Loading
Loading
Loading
+6 −3
Original line number Diff line number Diff line
@@ -492,7 +492,7 @@
    this.url = this.url.replace(/\/nojs(\/|$|\?|#)/, '/ajax$1');
    // If the 'nojs' version of the URL is trusted, also trust the 'ajax'
    // version.
    if (drupalSettings.ajaxTrustedUrl[originalUrl]) {
    if (drupalSettings.ajaxTrustedUrl.hasOwnProperty(originalUrl)) {
      drupalSettings.ajaxTrustedUrl[this.url] = true;
    }

@@ -569,7 +569,10 @@
        //   #ajax) can bypass header verification. This is especially useful
        //   for Ajax with multipart forms. Because IFRAME transport is used,
        //   the response headers cannot be accessed for verification.
        if (response !== null && !drupalSettings.ajaxTrustedUrl[ajax.url]) {
        if (
          response !== null &&
          !drupalSettings.ajaxTrustedUrl.hasOwnProperty(ajax.url)
        ) {
          if (xmlhttprequest.getResponseHeader('X-Drupal-Ajax-Token') !== '1') {
            const customMessage = Drupal.t(
              'The response failed verification so will not be processed.',
@@ -636,7 +639,7 @@
    // Bind the ajaxSubmit function to the element event.
    $(ajax.element).on(elementSettings.event, function (event) {
      if (
        !drupalSettings.ajaxTrustedUrl[ajax.url] &&
        !drupalSettings.ajaxTrustedUrl.hasOwnProperty(ajax.url) &&
        !Drupal.url.isLocal(ajax.url)
      ) {
        throw new Error(
+14 −0
Original line number Diff line number Diff line
@@ -168,3 +168,17 @@ ajax_test.link_page.dialog_contents:
    _controller: '\Drupal\ajax_test\Controller\AjaxTestController::httpMethodsDialog'
  requirements:
    _access: 'TRUE'

ajax_test.constructor:
  path: '/ajax-test/constructor'
  defaults:
    _controller: '\Drupal\ajax_test\Controller\AjaxTestController::constructor'
  requirements:
    _access: 'TRUE'

ajax_test.property_link:
  path: '/ajax-test/property-link'
  defaults:
    _controller: '\Drupal\ajax_test\Controller\AjaxTestController::propertyLink'
  requirements:
    _access: 'TRUE'
+29 −0
Original line number Diff line number Diff line
@@ -10,7 +10,9 @@
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Provides content for dialog tests.
@@ -526,4 +528,31 @@ public function linkPageDialogTitle(): string {
    return $title;
  }

  /**
   * Provides a valid AJAX response for the "constructor" URL.
   *
   * @return \Symfony\Component\HttpFoundation\Response
   *   The Ajax link.
   */
  public function constructor(): Response {
    return new JsonResponse();
  }

  /**
   * Provides an Ajax link to a URL that is also a JS object property.
   *
   * @return array
   *   The Ajax link.
   */
  public function propertyLink(): array {
    return [
      '#markup' => '<a class="use-ajax" href="constructor" data-ajax-http-method="GET">Ajax constructor</a>',
      '#attached' => [
        'library' => [
          'core/drupal.ajax',
        ],
      ],
    ];
  }

}
+17 −0
Original line number Diff line number Diff line
@@ -369,4 +369,21 @@ public function testAjaxFocus(): void {
    $this->assertEquals('edit-email-field-1', $has_focus_id);
  }

  /**
   * Tests URLs that are also a JS object property are not mistakenly trusted.
   */
  public function testPropertyUrl(): void {
    $this->drupalGet('ajax-test/property-link');
    $this->clickLink('Ajax constructor');

    // The AJAX request should fail because the URL is not trusted.
    $this->failOnJavascriptConsoleErrors = FALSE;
    $this->assertSession()
      ->statusMessageContainsAfterWait("Oops, something went wrong. Check your browser's developer console for more details.", 'error');

    // This is needed to avoid an unfinished AJAX request error from tearDown()
    // because this test intentionally does not complete all AJAX requests.
    $this->getSession()->executeScript("delete window.drupalActiveXhrCount");
  }

}