RouteProvider::getRouteCollectionForRequest() can poison query string of next request

Created on 3 October 2019, about 5 years ago
Updated 26 June 2024, 5 months ago

Problem/Motivation

Requests have their query parameters overridden by RouteProvider::getRouteCollectionForRequest() (here) when a route collection is matched in the cache. This is problematic if the cache entry was saved during a subrequest which alters the original request, specifically DefaultExceptionHtmlSubscriber, which sets two internal query parameters (_exception_statuscode and destination) for the error route.

This is problematic in so far as it's not intuitive behaviour (though this dates back to some early pre-8.0 work; see #2480811: Cache incoming path processing and route matching ā†’ ) but more importantly that the query parameters of later requests are overridden with those in the cache. For a form submission, this would mean the destination is overridden and incorrect, but in the case of a jsonapi path, it also means you get:

Drupal\\Core\\Http\\Exception\\CacheableBadRequestHttpException: The following query parameters violate the JSON:API spec: 'destination', '_exception_statuscode'. in Drupal\\jsonapi\\EventSubscriber\\JsonApiRequestValidator->validateQueryParams()

For instance, a common pattern in OAuth/JWT workflows is that a client will make a request with an existing access token; if it's unsuccessful (e.g., returns 401 - see #2840205: Error messages/codes should be more helpful & match spec. ā†’ ) then the client will attempt to obtain a new access token with a refresh token grant, and retry the original request. In this case, the retried request will fail with the above exception against a jsonapi endpoint as the now-authorized request will have the non-JSON:API spec compliant query string added.

Steps to reproduce

  1. As an unauthenticated user, request a json:api resource requiring authentication (e.g., a user.)
  2. Receive a 401 access denied error (expected.)
  3. Authenticate.
  4. Request the same resource; the site will error, complaining of improper destination and _exception_statuscode query parameters.
  5. Clear cache.
  6. Request the resource, again; on a clean cache, the request will process successfully.

Proposed resolution

The query string recovery from cache has mostly to do with the private files controller; see notes below. However, this issue is sensitive to alterations to the Request object prior to the cache ID generation for the collection cache, earlier even than incoming path processing.

Adjust the route collection cache cid to include the query parameters stored on the Request object after early processing.

Remaining tasks

Subsystem maintainer/core committer review.

User interface changes

None

API changes

None.

Data model changes

None.

Release notes snippet

Not necessary?

šŸ› Bug report
Status

Fixed

Version

10.3 āœØ

Component
RoutingĀ  ā†’

Last updated 1 day ago

Created by

šŸ‡ŗšŸ‡øUnited States bradjones1 Digital Nomad Life

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • The Needs Review Queue Bot ā†’ tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

    Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.

    Consult the Drupal Contributor Guide ā†’ to find step-by-step guides for working with issues.

  • First commit to issue fork.
  • last update about 1 year ago
    Custom Commands Failed
  • šŸ‡³šŸ‡±Netherlands bbrala Netherlands

    Commit was because of this discussion: https://drupal.slack.com/archives/C5A70F7D1/p1694441795848429

    With this patch there were issues with symfony 6. Last commit was to fix that.

  • last update about 1 year ago
    30,155 pass
  • Status changed to Needs review about 1 year ago
  • šŸ‡ŗšŸ‡øUnited States bradjones1 Digital Nomad Life
  • Pipeline finished with Success
    about 1 year ago
    Total: 2950s
    #20129
  • šŸ‡ØšŸ‡¦Canada joseph.olstad

    Great work on this one, it makes sense from a high level. Functionality appears to be covered by tests.

    One more thing that the core maintainers might like to see is a tests only run, I was about to do this but have been summoned elsewhere.

  • šŸ‡ŗšŸ‡øUnited States bradjones1 Digital Nomad Life

    There is a test-only MR that is a bit out of date but the previous experience with it at least demonstrates what the issue is. I can put in the work to update it if need be, however this subsystem is very low-change so hopefully it's clear enough from the issue history and prior runs.

  • šŸ‡ØšŸ‡¦Canada joseph.olstad

    @bradjones1 , it looks like @longwave comments have been resolved.

  • šŸ‡³šŸ‡±Netherlands bbrala Netherlands

    I've rebased the test only issue, cause i kinda want to see a fresh run of that as part of the review. Asked for a retarget in @contribute.

  • Status changed to Needs work about 1 year ago
  • šŸ‡³šŸ‡±Netherlands bbrala Netherlands

    There is still a few open comments on the main MR unfortunately, also awaiting retarget of test branch so we can have a recent test only run. (although that is almost automated on gitlab ci ;))

  • Pipeline finished with Failed
    about 1 year ago
    Total: 1347s
    #20329
  • last update about 1 year ago
    Custom Commands Failed
  • šŸ‡ŗšŸ‡øUnited States bradjones1 Digital Nomad Life

    bradjones1 ā†’ changed the visibility of the branch 3085360-exception-poison-test-only to hidden.

  • šŸ‡ŗšŸ‡øUnited States bradjones1 Digital Nomad Life

    bradjones1 ā†’ changed the visibility of the branch 3085360-exception-poison-11-x to hidden.

  • Pipeline finished with Canceled
    6 months ago
    Total: 18s
    #182384
  • Pipeline finished with Failed
    6 months ago
    Total: 194s
    #182386
  • Pipeline finished with Failed
    6 months ago
    Total: 590s
    #182388
  • Pipeline finished with Failed
    6 months ago
    Total: 169s
    #182418
  • Pipeline finished with Failed
    6 months ago
    Total: 626s
    #182424
  • Pipeline finished with Success
    6 months ago
    Total: 696s
    #182435
  • Pipeline finished with Failed
    6 months ago
    Total: 659s
    #182961
  • Status changed to Needs review 6 months ago
  • šŸ‡ŗšŸ‡øUnited States bradjones1 Digital Nomad Life

    Rebased and updated the cache ID creation logic to ensure it's normalized recursively with any nested arrays.

    Raised this PR against Symfony upstream to do so out of the box in the Request object, but I won't hold my breath.

    Optimistically marking this NR again.

  • Pipeline finished with Success
    6 months ago
    Total: 611s
    #182973
  • šŸ‡ŗšŸ‡øUnited States bradjones1 Digital Nomad Life
  • Status changed to RTBC 6 months ago
  • šŸ‡³šŸ‡±Netherlands bbrala Netherlands

    Al issues are addressed. Code still looks good. IS is still correct. Changerecord is not needed. All seems ready for commit.

  • šŸ‡¬šŸ‡§United Kingdom catch

    Somehow I haven't seen this issue before.

    My first thought was 'why do we allow incoming path processors to set query parameters?' and then I re-read the original issue that added the caching, realised it was opened by me, and it all came back.

    So the history of this bug is something like this:

    1. The router was rewritten on top of Symfony very early in the Drupal 8 cycle, it was railroaded through and almost designed from the start to erode performance, scalability and security (see #1793520-7: Add access control mechanism for new router system ā†’ and elsewhere for some background). A lot of things did not work with the initial commit, and only got found out about in some cases years later when we were still trying to release Drupal 8 (like e.g. the entire menu links system which didn't really start getting fixed again until about 2014).
    2. More than year later, the system files controller got converted #1987712: Convert file_download() to a new style controller ā†’ .
    3. PathProcessorFiles got added to deal with the fact that symfony doesn't like arbitrary/path/parts/when/looking/up/potential/routes
    4. We then added caching on top of of this, which had to take into account what PathProcessorFiles does to the query string in #2480811: Cache incoming path processing and route matching ā†’
    5. I think this is the first time any of 2-4 has been revisited since it was added.

    As the issue summary says this is all a bit problematic.

    I think my question is:

    Could PathProcessFiles set a request attribute instead of a query string, which we then retrieve in FileDownloadController? Then we'd need to save the request attribute(s) in the cache item instead of the query string, but because that's not overwriting information from the request it should be fine. For that specific use case we could almost add a generic 'original request path' request attribute that preserves what came in before path processors run.

    If we completely stop adding the query parameters to the cache item, that might break some other code somewhere that's doing something similar to PathProcessorFiles, but could we possibly only do that if the query parameters differ after incoming path processing has run, and trigger a deprecation error if they do? Then we could stop supporting that behaviour altogether in 12.x then

  • šŸ‡ŗšŸ‡øUnited States bradjones1 Digital Nomad Life

    Thanks for the review, @catch. I agree a request attribute would likely fix this - though the BC layer would have to look something like this MR anyway if we want it to actually work with caching. And given D10 is LTS, I think it's still worth "fixing" even if it contains a long-lived deprecation that gets cleaned up in D12. I agree it could be a second issue and worth cleaning up.

    TL;DR, I think we still need this bugfix regardless if we decide to change the underlying bug, since it's entirely possible people are using query strings internally as cheapo attributes.

  • šŸ‡¬šŸ‡§United Kingdom catch

    Opened šŸ“Œ Use attributes instead of query string in PathProcessorFiles and stop replacing query string in RouteProvider::getRouteCollectionForRequest() Active for the follow-up. Haven't done a line by line review here so leaving this at RTBC.

  • šŸ‡³šŸ‡±Netherlands kingdutch

    I just want to chime in here that the proposed PathProcessor is exactly what we implemented in Open Social where I found the problems with query on cacheable requests (which is why I assume the private file system didn't suffer) and moved to attributes instead.

    However, one issue we're seeing is that there is a difference between the initial uncached request and secondary requests that utilise the page cache. Specifically we're seeing that for the first request the attribute that's set is properly run through Drupal\Core\PathProcessor\PathProcessorDecode but for subsequent requests that doesn't happen. This causes the attribute to be inconsistently decoded or encoded which makes downstream code brittle and can cause errors.

    I'm not sure if that's fixed with the MR proposed here (I have to look at the code) but at least wanted to share that issue because maybe someone already knows how this MR might affect that behaviour.

  • šŸ‡³šŸ‡±Netherlands kingdutch

    This was shortly discussed on Slack where Bard added

    I would imagine attributes aren't currently cached because they weren't considered at the time the system was first built out and/or weren't thought to be something that would materially affect the cacheability of the response. As solutions have matured there's more of a need for this stuff, but the caching layer hasn't caught up

    And catch confirmed

    None of this particular code has been touched since about 2013, it was all stop gaps around the routing system which was extremely broken at the time.

    So in that sense the private file system will likely need attribute caching in šŸ“Œ Use attributes instead of query string in PathProcessorFiles and stop replacing query string in RouteProvider::getRouteCollectionForRequest() Active but that can likely happen as part of that issue and I don't see a reason to block this issue itself. We don't currently see a reason why attribute caching wouldn't work.

    Happy to leave this RTBC (also after looking at the code) :D

    • catch ā†’ committed fbe023ba on 10.3.x
      Issue #3085360 by bradjones1, josephdpurcell, Giuseppe87, ravi.shankar,...
    • catch ā†’ committed 5c3b1baf on 10.4.x
      Issue #3085360 by bradjones1, josephdpurcell, Giuseppe87, ravi.shankar,...
    • catch ā†’ committed ef8fee9c on 11.0.x
      Issue #3085360 by bradjones1, josephdpurcell, Giuseppe87, ravi.shankar,...
    • catch ā†’ committed 8b19e428 on 11.x
      Issue #3085360 by bradjones1, josephdpurcell, Giuseppe87, ravi.shankar,...
  • Status changed to Fixed 6 months ago
  • šŸ‡¬šŸ‡§United Kingdom catch

    Reviewed this again. I think we should try to get the follow-ups done so that we can stop caching by query string again, but what's here is good, and the test coverage should ensure that any changes we make in the follow-ups don't break things unexpectedly.

    Did my best with issue credit here but it's a very long issue and ctrl-f isn't working to find people's usernames properly for some reason.

    Committed/pushed to 11.x and cherry-picked back through to 10.3.x, thanks!

  • šŸ‡ŗšŸ‡øUnited States bradjones1 Digital Nomad Life

    Thanks!

    One quick clarification though:

    so that we can stop caching by query string again

    We've always cached by query string... the change here is that we are caching based on both the original query string and any changes after processing. Honestly I think this would have to remain even if we change the original reason for this being a bug. For instance, I am setting some "default" query parameters in a path processor on my current project, which I think is a legitimate use of this pattern. (Instead of core stashing away some metadata derived from the query string.) So there's additional discussion to be had about pulling support for this altogether. But yes, follow-ups definitely appropriate. Thanks for closing this out! Bolstering my reputation as a necromancer.

  • Pipeline finished with Success
    6 months ago
    Total: 206s
    #191647
  • Pipeline finished with Success
    6 months ago
    #191978
  • Pipeline finished with Running
    6 months ago
    #191980
  • Pipeline finished with Success
    6 months ago
    Total: 174s
    #192580
  • Pipeline finished with Success
    6 months ago
    Total: 149s
    #192775
  • Pipeline finished with Success
    6 months ago
    Total: 145s
    #193039
  • šŸ‡µšŸ‡¹Portugal marcofernandes

    Hi, I'm trying to apply a patch based on MR183 to 9.5.x but it's failing at TestRunnerKernel.php

    āžœ  drupal git:(9.5.x) āœ— patch -p1 < ./patches/183.patch
    patching file 'core/lib/Drupal/Core/Routing/RouteProvider.php'
    patching file 'core/modules/system/tests/modules/router_test_directory/router_test.module'
    patching file 'core/modules/system/tests/modules/router_test_directory/router_test.routing.yml'
    patching file 'core/modules/system/tests/modules/router_test_directory/src/RouterTestEarlyExceptionSubscriber.php'
    patching file 'core/modules/system/tests/modules/router_test_directory/src/RouterTestServiceProvider.php'
    patching file 'core/modules/system/tests/modules/router_test_directory/src/TestControllers.php'
    patching file 'core/tests/Drupal/FunctionalTests/Routing/RouteCachingQueryAlteredTest.php'
    patching file 'core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php'
    patching file 'core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php'
    patching file 'core/modules/system/tests/modules/router_test_directory/src/RouterTestEarlyExceptionSubscriber.php'
    patching file 'core/lib/Drupal/Core/Routing/RouteProvider.php'
    patching file 'core/lib/Drupal/Core/Routing/RouteProvider.php'
    patching file 'core/modules/system/tests/modules/router_test_directory/src/RouterTestEarlyExceptionSubscriber.php'
    patching file 'core/modules/system/tests/modules/router_test_directory/src/TestControllers.php'
    patching file 'core/tests/Drupal/FunctionalTests/Routing/RouteCachingQueryAlteredTest.php'
    patching file 'core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php'
    patching file 'core/lib/Drupal/Core/Routing/RouteProvider.php'
    patching file 'core/modules/system/tests/modules/router_test_directory/src/RouterTestEarlyExceptionSubscriber.php'
    patching file 'core/tests/Drupal/FunctionalTests/Routing/RouteCachingQueryAlteredTest.php'
    patching file 'core/lib/Drupal/Core/Routing/RouteProvider.php'
    patching file 'core/lib/Drupal/Core/Routing/RouteProvider.php'
    patching file 'core/lib/Drupal/Core/Test/TestRunnerKernel.php'
    1 out of 1 hunks failed--saving rejects to 'core/lib/Drupal/Core/Test/TestRunnerKernel.php.rej'
    patching file 'core/modules/system/tests/modules/router_test_directory/src/RouterTestEarlyExceptionSubscriber.php'
    patching file 'core/lib/Drupal/Core/Routing/RouteProvider.php'
    patching file 'core/modules/system/tests/modules/router_test_directory/src/TestControllers.php'
    patching file 'core/lib/Drupal/Core/Routing/RouteProvider.php'
    patching file 'core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php'
    āžœ  drupal git:(9.5.x) āœ— 
    

    Maybe related to the failed test on this commit?: https://git.drupalcode.org/project/drupal/-/merge_requests/183/diffs?com...

  • šŸ‡ŗšŸ‡øUnited States bradjones1 Digital Nomad Life

    @marcofernandes Commenting on a closed issue is unlikely to get you a response. Also, D9 is unsupported, so the general recommendation will be to upgrade first.

  • šŸ‡µšŸ‡¹Portugal marcofernandes

    @bradjones1 Yes, I understand that šŸ˜‰ but my comment was meant as a heads-up for people who are still in the 9.x and comes across this issue. The solution was just remove that rejected hunk.

  • Automatically closed - issue fixed for 2 weeks with no activity.

  • Pipeline finished with Failed
    5 months ago
    #223219
  • Pipeline finished with Success
    5 months ago
    Total: 1378s
    #223231
  • Pipeline finished with Success
    5 months ago
    Total: 1149s
    #223245
  • Pipeline finished with Canceled
    4 months ago
    Total: 164s
    #238816
  • Pipeline finished with Failed
    4 months ago
    Total: 1289s
    #238820
  • Pipeline finished with Success
    4 months ago
    Total: 1226s
    #238836
  • Pipeline finished with Failed
    4 months ago
    Total: 214s
    #251920
  • Pipeline finished with Canceled
    4 months ago
    #251921
  • Pipeline finished with Failed
    4 months ago
    Total: 228s
    #251922
  • Pipeline finished with Failed
    4 months ago
    #252702
  • Pipeline finished with Failed
    4 months ago
    Total: 216s
    #252739
  • Pipeline finished with Failed
    4 months ago
    #252773
  • Pipeline finished with Success
    4 months ago
    Total: 243s
    #252791
  • Pipeline finished with Success
    4 months ago
    Total: 373s
    #253588
  • Pipeline finished with Success
    4 months ago
    Total: 255s
    #253592
  • Pipeline finished with Success
    4 months ago
    Total: 331s
    #259131
  • Pipeline finished with Skipped
    3 months ago
    #260079
  • Pipeline finished with Success
    3 months ago
    Total: 239s
    #274539
  • Pipeline finished with Success
    3 months ago
    Total: 334s
    #279318
  • Pipeline finished with Success
    2 months ago
    Total: 979s
    #300873
  • Pipeline finished with Success
    about 2 months ago
    Total: 161s
    #307569
  • Pipeline finished with Success
    about 2 months ago
    Total: 290s
    #307643
  • Pipeline finished with Success
    about 2 months ago
    Total: 230s
    #307651
  • Pipeline finished with Success
    about 2 months ago
    Total: 142s
    #307838
  • Pipeline finished with Success
    about 2 months ago
    Total: 139s
    #307858
  • Pipeline finished with Success
    about 2 months ago
    Total: 134s
    #307862
  • Pipeline finished with Failed
    about 2 months ago
    Total: 160s
    #309550
  • Pipeline finished with Success
    about 2 months ago
    Total: 166s
    #309565
  • Pipeline finished with Canceled
    about 2 months ago
    Total: 89s
    #309595
  • Pipeline finished with Failed
    about 2 months ago
    Total: 168s
    #309596
  • Pipeline finished with Success
    about 2 months ago
    Total: 162s
    #309597
  • Pipeline finished with Canceled
    about 2 months ago
    Total: 91s
    #310121
  • Pipeline finished with Failed
    about 2 months ago
    Total: 224s
    #310122
  • Pipeline finished with Success
    about 2 months ago
    Total: 191s
    #310137
  • Pipeline finished with Success
    about 2 months ago
    Total: 308s
    #311116
  • Pipeline finished with Success
    about 1 month ago
    Total: 303s
    #327775
  • Pipeline finished with Canceled
    22 days ago
    Total: 190s
    #335546
  • Pipeline finished with Failed
    22 days ago
    Total: 606s
    #335548
  • Pipeline finished with Failed
    22 days ago
    Total: 453s
    #335569
  • Pipeline finished with Canceled
    22 days ago
    Total: 100s
    #335576
  • Pipeline finished with Failed
    22 days ago
    Total: 95s
    #335578
  • Pipeline finished with Failed
    22 days ago
    Total: 94s
    #335583
  • Pipeline finished with Failed
    22 days ago
    Total: 108s
    #335595
  • Pipeline finished with Failed
    22 days ago
    Total: 96s
    #335599
  • Pipeline finished with Failed
    22 days ago
    Total: 543s
    #335604
  • Pipeline finished with Canceled
    22 days ago
    Total: 196s
    #335615
  • Pipeline finished with Canceled
    22 days ago
    Total: 64s
    #335619
  • Pipeline finished with Failed
    22 days ago
    Total: 95s
    #335621
  • Pipeline finished with Failed
    22 days ago
    Total: 610s
    #335623
  • Pipeline finished with Failed
    22 days ago
    #335629
  • Pipeline finished with Canceled
    22 days ago
    Total: 210s
    #335636
  • Pipeline finished with Success
    22 days ago
    Total: 860s
    #335646
  • Pipeline finished with Success
    22 days ago
    Total: 830s
    #335655
  • Pipeline finished with Success
    11 days ago
    Total: 154s
    #347284
  • Pipeline finished with Success
    9 days ago
    Total: 448s
    #348597
  • Pipeline finished with Success
    9 days ago
    Total: 408s
    #348600
Production build 0.71.5 2024