Skip to content
Snippets Groups Projects
Commit 8b19e428 authored by catch's avatar catch
Browse files

Issue #3085360 by bradjones1, josephdpurcell, Giuseppe87, ravi.shankar,...

Issue #3085360 by bradjones1, josephdpurcell, Giuseppe87, ravi.shankar, rajandro, ridhimaabrol24, bbrala, andregp, jhedstrom: RouteProvider::getRouteCollectionForRequest() can poison query string of next request
parent d7f114cd
Branches
Tags
32 merge requests!12227Issue #3181946 by jonmcl, mglaman,!11131[10.4.x-only-DO-NOT-MERGE]: Issue ##2842525 Ajax attached to Views exposed filter form does not trigger callbacks,!9470[10.3.x-only-DO-NOT-MERGE]: #3331771 Fix file_get_contents(): Passing null to parameter,!8736Update the Documention As per the Function uses.,!8540Issue #3457061: Bootstrap Modal dialog Not closing after 10.3.0 Update,!8528Issue #3456871 by Tim Bozeman: Support NULL services,!8513Issue #3453786: DefaultSelection should document why values for target_bundles NULL and [] behave as they do,!8373Issue #3427374 by danflanagan8, Vighneshh: taxonomy_tid ViewsArgumentDefault...,!8256Issue #3445896 by mstrelan, mondrake: PHPUnit\Runner\ErrorHandler::__construct...,!8126Added escape fucntionality on admintoolbar close icon,!5423Draft: Resolve #3329907 "Test2",!3878Removed unused condition head title for views,!3818Issue #2140179: $entity->original gets stale between updates,!3742Issue #3328429: Create item list field formatter for displaying ordered and unordered lists,!3731Claro: role=button on status report items,!3651Issue #3347736: Create new SDC component for Olivero (header-search),!3531Issue #3336994: StringFormatter always displays links to entity even if the user in context does not have access,!3478Issue #3337882: Deleted menus are not removed from content type config,!3355Issue #3209129: Scrolling problems when adding a block via layout builder,!3154Fixes #2987987 - CSRF token validation broken on routes with optional parameters.,!3133core/modules/system/css/components/hidden.module.css,!2964Issue #2865710 : Dependencies from only one instance of a widget are used in display modes,!2812Issue #3312049: [Followup] Fix Drupal.Commenting.FunctionComment.MissingReturnType returns for NULL,!2378Issue #2875033: Optimize joins and table selection in SQL entity query implementation,!2062Issue #3246454: Add weekly granularity to views date sort,!1105Issue #3025039: New non translatable field on translatable content throws error,!10223132456: Fix issue where views instances are emptied before an ajax request is complete,!877Issue #2708101: Default value for link text is not saved,!617Issue #3043725: Provide a Entity Handler for user cancelation,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493
Pipeline #186463 passed
Pipeline: drupal

#186465

    ......@@ -453,6 +453,8 @@ protected function getRouteCollectionCacheId(Request $request) {
    // based on the domain.
    $this->addExtraCacheKeyPart('language', $this->getCurrentLanguageCacheIdPart());
    $this->addExtraCacheKeyPart('query_parameters', $this->getQueryParametersCacheIdPart($request));
    // Sort the cache key parts by their provider in order to have predictable
    // cache keys.
    ksort($this->extraCacheKeyParts);
    ......@@ -461,7 +463,52 @@ protected function getRouteCollectionCacheId(Request $request) {
    $key_parts[] = '[' . $provider . ']=' . $key_part;
    }
    return 'route:' . implode(':', $key_parts) . ':' . $request->getPathInfo() . ':' . $request->getQueryString();
    return 'route:' . implode(':', $key_parts) . ':' . $request->getPathInfo();
    }
    /**
    * Returns the query parameters identifier for the route collection cache.
    *
    * The query parameters on the request may be altered programmatically, e.g.
    * while serving private files or in subrequests. As such, we must vary on
    * both the query string from the client and the parameter bag after incoming
    * route processors have modified the request object.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
    * Request.
    *
    * @return string
    */
    protected function getQueryParametersCacheIdPart(Request $request) {
    // @todo Use \Symfony\Component\HttpFoundation\Request::normalizeQueryString
    // for recursive key ordering if support is added in the future.
    $recursive_sort = function (&$array) use (&$recursive_sort) {
    foreach ($array as &$v) {
    if (is_array($v)) {
    $recursive_sort($v);
    }
    }
    ksort($array);
    };
    // Recursively normalize the query parameters to ensure maximal cache hits.
    // If we did not normalize the order, functionally identical query string
    // sets could be sent in differing order creating a potential DoS vector
    // and decreasing cache hit rates.
    $sorted_resolved_parameters = $request->query->all();
    $recursive_sort($sorted_resolved_parameters);
    $sorted_original_parameters = Request::create('/?' . $request->getQueryString())->query->all();
    $recursive_sort($sorted_original_parameters);
    // Hash this portion to help shorten the total key length.
    $resolved_hash = $sorted_resolved_parameters
    ? sha1(http_build_query($sorted_resolved_parameters))
    : NULL;
    return implode(
    ',',
    array_filter([
    http_build_query($sorted_original_parameters),
    $resolved_hash,
    ])
    );
    }
    /**
    ......
    <?php
    /**
    * @file
    * Test module.
    */
    use Drupal\Core\Url;
    /**
    * Implements hook_preprocess_HOOK().
    *
    * Performs an operation that calls the RouteProvider's collection method
    * during an exception page view. (which is rendered during a subrequest.)
    *
    * @see \Drupal\FunctionalTests\Routing\RouteCachingQueryAlteredTest
    */
    function router_test_preprocess_page(&$variables) {
    $request = \Drupal::request();
    if ($request->getPathInfo() === '/router-test/rejects-query-strings') {
    // Create a URL from the request, e.g. for a breadcrumb or other contextual
    // information.
    Url::createFromRequest($request);
    }
    }
    ......@@ -247,3 +247,10 @@ router_test.case_sensitive_duplicate3:
    _controller: '\Drupal\router_test\TestControllers::testRouteName'
    requirements:
    _access: 'TRUE'
    router_test.rejects_query_strings:
    path: '/router-test/rejects-query-strings'
    defaults:
    _controller: '\Drupal\router_test\TestControllers::rejectsQueryStrings'
    requirements:
    _access: 'TRUE'
    <?php
    declare(strict_types=1);
    namespace Drupal\router_test;
    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\HttpKernel\Event\RequestEvent;
    use Symfony\Component\HttpKernel\Exception\HttpException;
    use Symfony\Component\HttpKernel\KernelEvents;
    /**
    * Event subscribers for exceptions thrown in early kernel middleware.
    */
    class RouterTestEarlyExceptionSubscriber implements EventSubscriberInterface {
    /**
    * Throw an exception, which will trigger exception-handling subscribers.
    *
    * See DefaultExceptionHtmlSubscriber.
    */
    public function onKernelRequest(RequestEvent $event): void {
    if ($event->isMainRequest() && $event->getRequest()->headers->get('Authorization') === 'Bearer invalid') {
    throw new HttpException(
    Response::HTTP_UNAUTHORIZED,
    'This is a common exception during authentication.'
    );
    }
    }
    /**
    * {@inheritdoc}
    */
    public static function getSubscribedEvents(): array {
    // This is the same priority as AuthenticationSubscriber, however
    // exceptions are not restricted to authentication; this is a common,
    // early point to emulate an exception, e.g. when an OAuth token is
    // rejected.
    $events[KernelEvents::REQUEST][] = ['onKernelRequest', 300];
    return $events;
    }
    }
    ......@@ -14,9 +14,12 @@ class RouterTestServiceProvider implements ServiceProviderInterface {
    * {@inheritdoc}
    */
    public function register(ContainerBuilder $container) {
    $container->register('router_test.subscriber', 'Drupal\router_test\RouteTestSubscriber')->addTag('event_subscriber');
    $container->register('router_test.subscriber', 'Drupal\router_test\RouteTestSubscriber')
    ->addTag('event_subscriber');
    $container->register('access_check.router_test', 'Drupal\router_test\Access\TestAccessCheck')
    ->addTag('access_check', ['applies_to' => '_access_router_test']);
    $container->register('router_test.early_exception.subscriber', 'Drupal\router_test\RouterTestEarlyExceptionSubscriber')
    ->addTag('event_subscriber');
    }
    }
    ......@@ -118,6 +118,19 @@ public function testRouteName(Request $request) {
    ];
    }
    /**
    * Rejects requests with query keys.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
    * The given request.
    *
    * @return \Symfony\Component\HttpFoundation\Response
    * The response.
    */
    public function rejectsQueryStrings(Request $request) {
    return new Response('', $request->query->keys() ? Response::HTTP_BAD_REQUEST : Response::HTTP_OK);
    }
    /**
    * Throws an exception.
    *
    ......
    <?php
    declare(strict_types=1);
    namespace Drupal\FunctionalTests\Routing;
    use Drupal\Tests\BrowserTestBase;
    use Symfony\Component\HttpFoundation\Response;
    /**
    * Tests the route cache when the request's query parameters are altered.
    *
    * This happens either in the normal course of operations or due to an
    * exception.
    *
    * @group routing
    */
    class RouteCachingQueryAlteredTest extends BrowserTestBase {
    /**
    * {@inheritdoc}
    */
    protected static $modules = ['router_test'];
    /**
    * {@inheritdoc}
    */
    protected $defaultTheme = 'stark';
    /**
    * {@inheritdoc}
    */
    protected function setUp(): void {
    parent::setUp();
    // page_cache module is enabled in the testing profile, however by default
    // exceptions which create 4xx responses are cached for 1 hour. This is
    // undesirable for certain response types (e.g., 401) which vary on other
    // elements of the request than the URL. For this reason, do not cache 4xx
    // responses for the purposes of this test.
    $settings['settings']['cache_ttl_4xx'] = (object) [
    'value' => 0,
    'required' => TRUE,
    ];
    $this->writeSettings($settings);
    }
    /**
    * Tests route collection cache after an exception.
    */
    public function testRouteCollectionCacheAfterException() {
    // Force an exception early in the Kernel middleware on a cold cache by
    // simulating bad Bearer authentication.
    $this->drupalGet('/router-test/rejects-query-strings', [], [
    'Authorization' => 'Bearer invalid',
    ]);
    $this->assertSession()->statusCodeEquals(Response::HTTP_UNAUTHORIZED);
    // Check that the route collection cache does not recover any unexpected
    // query strings from the earlier request that involved an exception.
    // The requested controller returns 400 if there are any query parameters
    // present, similar to JSON:API paths that strictly filter requests.
    $this->drupalGet('/router-test/rejects-query-strings', [], [
    'Authorization' => 'Bearer valid',
    ]);
    $this->assertSession()->statusCodeEquals(Response::HTTP_OK);
    }
    }
    ......@@ -584,7 +584,7 @@ public function testRouteCaching() {
    $request = Request::create($path, 'GET');
    $provider->getRouteCollectionForRequest($request);
    $cache = $this->cache->get('route:[language]=en:/path/add/one:');
    $cache = $this->cache->get('route:[language]=en:[query_parameters]=:/path/add/one');
    $this->assertEquals('/path/add/one', $cache->data['path']);
    $this->assertEquals([], $cache->data['query']);
    $this->assertCount(3, $cache->data['routes']);
    ......@@ -594,17 +594,33 @@ public function testRouteCaching() {
    $request = Request::create($path, 'GET');
    $provider->getRouteCollectionForRequest($request);
    $cache = $this->cache->get('route:[language]=en:/path/add/one:foo=bar');
    $cache = $this->cache->get('route:[language]=en:[query_parameters]=foo=bar,2fb8f40115dd1e695cbe23d4f97ce5b1fb697eee:/path/add/one');
    $this->assertEquals('/path/add/one', $cache->data['path']);
    $this->assertEquals(['foo' => 'bar'], $cache->data['query']);
    $this->assertCount(3, $cache->data['routes']);
    // A path with multivalued query parameters.
    $path = '/path/add/one?foo=bar&foo2[]=bar2&foo2[]=bar3';
    $request = Request::create($path, 'GET');
    $provider->getRouteCollectionForRequest($request);
    $cache = $this->cache->get('route:[language]=en:[query_parameters]=foo=bar&foo2%5B0%5D=bar2&foo2%5B1%5D=bar3,2d3a0851c4970a16be1c851a0e9946e6d10a3fe2:/path/add/one');
    $this->assertEquals('/path/add/one', $cache->data['path']);
    $this->assertEquals(
    [
    'foo' => 'bar',
    'foo2' => ['bar2', 'bar3'],
    ],
    $cache->data['query']
    );
    $this->assertCount(3, $cache->data['routes']);
    // A path with placeholders.
    $path = '/path/1/one';
    $request = Request::create($path, 'GET');
    $provider->getRouteCollectionForRequest($request);
    $cache = $this->cache->get('route:[language]=en:/path/1/one:');
    $cache = $this->cache->get('route:[language]=en:[query_parameters]=:/path/1/one');
    $this->assertEquals('/path/1/one', $cache->data['path']);
    $this->assertEquals([], $cache->data['query']);
    $this->assertCount(2, $cache->data['routes']);
    ......@@ -619,7 +635,7 @@ public function testRouteCaching() {
    $request = Request::create($path, 'GET');
    $provider->getRouteCollectionForRequest($request);
    $cache = $this->cache->get('route:[language]=en:/path/add-one:');
    $cache = $this->cache->get('route:[language]=en:[query_parameters]=:/path/add-one');
    $this->assertEquals('/path/add/one', $cache->data['path']);
    $this->assertEquals([], $cache->data['query']);
    $this->assertCount(3, $cache->data['routes']);
    ......@@ -634,7 +650,7 @@ public function testRouteCaching() {
    $request = Request::create($path, 'GET');
    $provider->getRouteCollectionForRequest($request);
    $cache = $this->cache->get('route:[language]=gsw-berne:/path/add-one:');
    $cache = $this->cache->get('route:[language]=gsw-berne:[query_parameters]=:/path/add-one');
    $this->assertEquals('/path/add/one', $cache->data['path']);
    $this->assertEquals([], $cache->data['query']);
    $this->assertCount(3, $cache->data['routes']);
    ......
    • catch @catch

      mentioned in commit fbe023ba

      ·

      mentioned in commit fbe023ba

      Toggle commit list
    • catch @catch

      mentioned in commit 5c3b1baf

      ·

      mentioned in commit 5c3b1baf

      Toggle commit list
    • catch @catch

      mentioned in commit ef8fee9c

      ·

      mentioned in commit ef8fee9c

      Toggle commit list
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Please register or to comment