Problem/Motivation
The warning occurs during every use of the pager.parameters service and when the GET query is not page=1 but something array-like, e.g. page[]=1.
This sort of query can easily happen due to bugs in javascript or triggered by automated scanners.
Steps to reproduce
Visit a page with a pager, like the recipe page in umami, and add the query argument page[]=1
This will trigger the following exception in the watchdog:
Symfony\Component\HttpKernel\Exception\BadRequestHttpException: Input value "page" contains a non-scalar value. in Symfony\Component\HttpKernel\HttpKernel->handle() (line 83 of /app/vendor/symfony/http-kernel/HttpKernel.php).
This is triggered by a previous exception that has a stack trace/signature similar to this from the original report:
Warning: explode() expects parameter 2 to be string, array given in Drupal\Core\Pager\PagerParameters->getPagerQuery() (line 58 of /home/catuser/app/public/docroot/core/lib/Drupal/Core/Pager/PagerParameters.php)
#0 /home/catuser/app/public/docroot/core/includes/bootstrap.inc(600): _drupal_error_handler_real(2, 'explode() expec...', '/home/catuser/a...', 58, Array)
#1 [internal function]: _drupal_error_handler(2, 'explode() expec...', '/home/catuser/a...', 58, Array)
#2 /home/catuser/app/public/docroot/core/lib/Drupal/Core/Pager/PagerParameters.php(58): explode(',', Array)
#3 /home/catuser/app/public/docroot/core/lib/Drupal/Core/Pager/PagerParameters.php(49): Drupal\Core\Pager\PagerParameters->getPagerQuery()
#4 /home/catuser/app/public/docroot/core/lib/Drupal/Core/Pager/PagerManager.php(49): Drupal\Core\Pager\PagerParameters->findPage(0)
#5 /home/catuser/app/public/docroot/core/modules/views/src/Plugin/views/pager/SqlBase.php(341): Drupal\Core\Pager\PagerManager->createPager('1470', 25, 0)
#6 /home/catuser/app/public/docroot/core/modules/views/src/ViewExecutable.php(1422): Drupal\views\Plugin\views\pager\SqlBase->updatePageInfo()
#7 /home/catuser/app/public/docroot/core/modules/views/src/ViewExecutable.php(1454): Drupal\views\ViewExecutable->execute(NULL)
#8 /home/catuser/app/public/docroot/core/modules/rest/src/Plugin/views/display/RestExport.php(441): Drupal\views\ViewExecutable->render()
#9 /home/catuser/app/public/docroot/core/modules/views/src/ViewExecutable.php(1630): Drupal\rest\Plugin\views\display\RestExport->execute()
#10 /home/catuser/app/public/docroot/core/modules/views/src/Element/View.php(77): Drupal\views\ViewExecutable->executeDisplay('json_seo_dashbo...', Array)
#11 [internal function]: Drupal\views\Element\View::preRenderViewElement(Array)
#12 /home/catuser/app/public/docroot/core/lib/Drupal/Core/Security/DoTrustedCallbackTrait.php(100): call_user_func_array(Array, Array)
#13 /home/catuser/app/public/docroot/core/lib/Drupal/Core/Render/Renderer.php(781): Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_ren...', 'silenced_deprec...', 'Drupal\\Core\\Ren...')
#14 /home/catuser/app/public/docroot/core/lib/Drupal/Core/Render/Renderer.php(372): Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array)
#15 /home/catuser/app/public/docroot/core/lib/Drupal/Core/Render/Renderer.php(200): Drupal\Core\Render\Renderer->doRender(Array, true)
#16 /home/catuser/app/public/docroot/core/lib/Drupal/Core/Render/Renderer.php(144): Drupal\Core\Render\Renderer->render(Array, true)
#17 /home/catuser/app/public/docroot/core/lib/Drupal/Core/Render/Renderer.php(573): Drupal\Core\Render\Renderer->Drupal\Core\Render\{closure}()
#18 /home/catuser/app/public/docroot/core/lib/Drupal/Core/Render/Renderer.php(145): Drupal\Core\Render\Renderer->executeInRenderContext(Object(Drupal\Core\Render\RenderContext), Object(Closure))
#19 /home/catuser/app/public/docroot/core/modules/rest/src/Plugin/views/display/RestExport.php(424): Drupal\Core\Render\Renderer->renderRoot(Array)
#20 /home/catuser/app/public/docroot/core/modules/views/src/Routing/ViewPageController.php(54): Drupal\rest\Plugin\views\display\RestExport::buildResponse('url_list', 'json_seo_dashbo...', Array)
#21 [internal function]: Drupal\views\Routing\ViewPageController->handle('url_list', 'json_seo_dashbo...', Object(Drupal\Core\Routing\RouteMatch))
#22 /home/catuser/app/public/docroot/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(123): call_user_func_array(Array, Array)
#23 /home/catuser/app/public/docroot/core/lib/Drupal/Core/Render/Renderer.php(573): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}()
#24 /home/catuser/app/public/docroot/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(124): Drupal\Core\Render\Renderer->executeInRenderContext(Object(Drupal\Core\Render\RenderContext), Object(Closure))
#25 /home/catuser/app/public/docroot/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(97): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array)
#26 /home/catuser/app/public/vendor/symfony/http-kernel/HttpKernel.php(151): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}()
#27 /home/catuser/app/public/vendor/symfony/http-kernel/HttpKernel.php(68): Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object(Symfony\Component\HttpFoundation\Request), 1)
#28 /home/catuser/app/public/docroot/core/lib/Drupal/Core/StackMiddleware/Session.php(57): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#29 /home/catuser/app/public/docroot/core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php(47): Drupal\Core\StackMiddleware\Session->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#30 /home/catuser/app/public/docroot/core/modules/page_cache/src/StackMiddleware/PageCache.php(106): Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#31 /home/catuser/app/public/docroot/core/modules/page_cache/src/StackMiddleware/PageCache.php(85): Drupal\page_cache\StackMiddleware\PageCache->pass(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#32 /home/catuser/app/public/vendor/asm89/stack-cors/src/Asm89/Stack/Cors.php(49): Drupal\page_cache\StackMiddleware\PageCache->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#33 /home/catuser/app/public/docroot/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(47): Asm89\Stack\Cors->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#34 /home/catuser/app/public/docroot/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(52): Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#35 /home/catuser/app/public/vendor/stack/builder/src/Stack/StackedHttpKernel.php(23): Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#36 /home/catuser/app/public/docroot/core/lib/Drupal/Core/DrupalKernel.php(708): Stack\StackedHttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#37 /home/catuser/app/public/docroot/index.php(19): Drupal\Core\DrupalKernel->handle(Object(Symfony\Component\HttpFoundation\Request))
#38 {main}
Proposed resolution
Make the code in the pager service more defensive to query values outside the expected structure.
Remaining tasks
n/a
User interface changes
n/a
API changes
n/a
Data model changes
n/a
Release notes snippet
n/a
| Comment | File | Size | Author |
|---|
Issue fork drupal-3143617
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
br0kenComment #3
br0kenProof.
Comment #4
br0kenComment #5
daffie commented@BROkEN: Thank you for this bug report, fix and the added tests.
I do not think that we should put the part
$page = $request->query->get('page')in the if-statement. Put it on its own line just before the return-statement.Can we change the line to:
$this->drupalGet('admin/reports/dblog', ['query' => ['page' => ['offset' => 1]]]);Comment #6
pavnish commentedComment #7
pavnish commented@BROkEN i will be very happy if i can do something in this issue so i assign this to my self.
Thanks
Pavnish
Comment #8
pavnish commented@daffie I have changed the code as suggest by you in #5.
It's my honour if you can review this patch.
Thanks
Pavnish
Comment #9
br0ken@daffie Yeah, that also working variant but in terms of efficiency it's a bit worse since
is_stringwill be called onnullall the times thepageis not in the GET query.Comment #10
daffie commentedUploading patch from comment #8 with only the added test. This patch should fail.
Comment #12
pavnish commentedComment #13
pavnish commented@daffie
Upload all the patch #8 with fix and test only patch
Comment #14
daffie commentedThe patch contains a bugfix and testing for the bugfix.
All code changes look good to me.
For me it is RTBC.
@BROkEN Likes to see another bugfix for this patch:
I leave it to the committer to choose which bugfix to commit.
Comment #17
durgeshs commentedComment #18
shaktikHi @daffie,
made changes please review.
Comment #19
br0kenI cannot understand why this easy issue produced such a lot of reinvented solution of what I initially posted.
#5.1 The reason to not do this is because it's not a common syntax in Drupal core, isn't it?
#5.2 Of course we can. We can even reformat it to multiline.
I am super frustrated that the issue like this becomes blocked due to the far-fetched problems.
@pavnish, @durgeshs, @shaktik please stop assigning this issue on yourselves. I'm able to complete it even if each new reviewer will say that we should put brackets as they want.
Comment #20
br0kenNo interdiff since the patch is small and easy to read.
Comment #21
br0ken#5.1 was actually correct not in terms of code formatting, but in terms of functionality where
'0'might have been skipped.Comment #22
shaktikHi @BR0kEN,
Please assign the issue to yourself or add a comment that you are working on this and please try to provide a solution within 1 or 2 days. If it will take more time to complete this please add a comment or draft patch otherwise community members will pick this which is very normal. Please check the guidelines
Comment #23
chi commentedThe
$request->queryobject offers a few helper methods to deal with this. LikegetDigits()andgetInt(). We could use them.Comment #24
jungleRe #23,
$request->query->getDigits(),$request->query->getInt()and other variations are inapplicable here, aspageis a comma-delimited string of pager element values. For example:?page=5,2,?page=2.1,6.999,,,5.Fixing one nitpick: Test that -> Tests that
Comment #26
jungleRe-queued
Comment #27
chi commented@jungle good point.
For the record, retrieving non-string values with
$request->query->get()method was deprecated in Symfony 5 and will throw an exception in Symfony 6.https://github.com/symfony/symfony/pull/34363
Comment #28
neclimdulYeah, please don't assign core issues. Those guidelines specifically says to avoid it and for something like this it generally just adds noise to the discussion. If you're going to propose a different approach just provide a patch without 3 issue updates.
That Symfony changes is pretty frustrating. I guess there's not a great solution to the ambiguity and dealing with "user" input but throwing an exception... seems like the method is becoming unusable. Something to consider for 10,
I was going to +1 the RTBC because in a narrow scope this obviously addresses the issue. But reading the patch one last time one questions lead to another, then a full on review, then digging in more I ended up rewriting the patch trying to sort out the answers. I don't think this fully addresses the problems with these methods.
Here's the review:
1) Why is this a functional test. It should just be a unit test, The extra complexity, boilerplating and slowness provides no benefit.
2) Why the link to the issue in the test comment? Isn't the git log good enough?
3) Why are we only testing one value? This doesn't fully flesh out the possible values affected by this change.
4) Why do we care if its a string? Could it be numeric? Parameter bags definitely allow that. We only care that its not an array right so is_scalar and a string cast seems more resilient.
5) Scope creapish, why is getPagerQuery using empty? The current patch documents how this will fail but only fixes it in getPagerParameter. findPage fixes handles the edge case and since its main entry point in core so functional tests aren't catching this method being broken.
6) More scope creapish, explode() returns an array of _strings_ but the interface requires returning int[]. Fixing it to match the interface _could_ break contrib or sites hacking strings or floats through a pager value. However those cases would be broken in findPage which does the cast already so it was probably already effectively broken. They can still access the request so I think we should fix it. Also not fixing it makes testing inconsistent.
So here's a patch
1) Converted to unit test.
2) Removed in functional test removal.
3) Added edge case assertions around NULL, 0, '0' and []. Tested these for getQuery, getQueryParameter, and findPage to assert they work consistently.
4) fixed
5) fixed
6) fixed
Comment #29
chi commented@neclimdul Neither patch #24 nor #28 adds any tests. Have I missed something?
Comment #31
neclimdulInsomnia review/patches... am i right? I diffed off the wrong branch, sorry.
The failure looks to have been random. Here's the patch I _meant_ to upload in 28.
Comment #32
neclimdulComment #38
needs-review-queue-bot commentedThe 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.
Comment #40
douggreen commentedAttached patch fixes issue raised by the bot. It also cleans up the comment a tad.
* The both complained about the
isset()which was introduced in #28, I've replace it with a simple condition because IMO neitherisset()nor!emptyis necessary here.Comment #41
douggreen commentedComment #43
neclimdulIts been 3 years so its hard to remember but pretty sure the isset was the to preserve the behavior of the empty check as demonstrated by the failing tests. What was the problem with it?
Comment #44
robertom commentedAttached patch fixes issues
raised by the test bot
Comment #45
robertom commentedComment #46
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue.
While you are making the above changes, we recommend that you convert this patch to a merge request. Merge requests are preferred over patches. Be sure to hide the old patch files as well. (Converting an issue to a merge request without other contributions to the issue will not receive credit.)
Comment #47
robertom commentedAttached a patch without the use of ->all()['page'] for avoid warning like
Comment #48
smustgrave commentedIssue summary should be updated to the standard issue template, even if sections don't apply leaving them with NA is easier for reviews/committers.
Comment #49
neclimdulUpdated.
Turns out its been long enough a few things have changed. Specifically, current versions of PHP throw an exception instead of a warning which makes it a bit harder to see. Specifically, real exception gets gobbled up by a more generic exception and Drupal's exception logging in watchdog doesn't show previous exceptions.
Since the actual exception is hidden and its hard to validate the root cause without hacking the exception handler or using a debugger, I've left the original trace in place. The fix is the same and the trace captures the essence of the problem.
Comment #50
smustgrave commentedRan patch #47 and it has a test failure that seems legit.
Recommended to convert to MRs
Comment #52
neclimdul"pretty sure the isset was the to preserve the behavior of the empty check as demonstrated by the failing tests." 🤷
Added it back because it was there to defensively handle a null value from the getPagerParameter method. Since isset is a language level feature there's no real cost to being defensive like this.
I'm already tired of the 500 remotes and all the commit juggling. I'm going to really miss the simple patch workflow. But ok, its in a MR now.
Comment #53
neclimdulTurns around it was a little more complicated. I went ahead and took the opportunity to update the tests and make things a little more clear.
Comment #54
neclimdulComment #55
smustgrave commentedThanks looks good!
Comment #56
quietone commentedI'm triaging RTBC issues. I read the IS, the comments and the MR.
Thanks for updating the Issue Summary, that is always helpful! The review in #28 by @neclimdul was also useful as it explained what they were thinking and checking. That provided me with insight into understanding the issue and when else I may need to check.
The changes to getPagerQuery and getPagerParameter are changing the behavior, making it conform to the interface. So, it there any ramification for contrib/custom?
I left suggestions in the MR for coding standards and one asking for documentation.
I hide all the patches.
Comment #57
smustgrave commentedThis issue got reported as a security issue for better_exposed_filter, but was later made public. #3466314: Inclusion of parameter exhausts site memory with exposed filter block
Addressed the feedback
Comment #58
feyp commentedThanks for working on this.
Regarding @quietone's outstanding question in #56:
I think this question has already been answered by @neclimdul in #28 6):
The code changes look good. I added one suggestion and a few comments inline, mostly concerning comments in the test.
The MR introduces new test coverage for the pager parameter. The pipeline is green. The tests fail locally as expected without the fix. I'll run the test only job just before I'm otherwise ready to RTBC, so not quite yet. With the MR applied, I can't reproduce the original issue. I also did some manual testing both with the original and modified (invalid) pager parameters: The administrative content view (using AJAX), a regular view (without AJAX), multiple views with different pager ids on the same page, the administrative URL Alias overview page (no view, probably entity query pager?). I noticed an issue with 3 views on the same page, but re-testing this without the MR applied, the issue was already present before, so no regression.
Tagging security improvement (for contrib) based on #57.
So overall this looks good and almost ready. Ready to RTBC once my very minor MR comments have been addressed.
Comment #59
smustgrave commentedDid not mean to take so long but gave it a shot. May need to expand on the docs some though.
Comment #60
feyp commentedThanks Stephen, the changes look good to me.
Looks like the CI runners are a little busy right now, the repeated failures due to GitHub timeouts and no test nodes available gave me sufficient time to also retest this locally. Almost like Drupal CI, oh the good old times, where I could work on another issue while I waited for the tests to complete... Let's call it the DrupalCon review bonus :)
Test only job fails as expected:
The documentation for the data provider return value could always be a bit more verbose of course, but I think documenting that the keys returned by the data provider correspond to the variable names that are also used in the test methods should be enough for developers looking at this test code in the future to get the idea behind this setup. It should also address @quietone's concern:
So let's give the RTBC queue triage team another shot at this... I think this issue is RTBC 🎉!
Comment #61
quietone commentedWho are members of this team? :-)
Thanks for updating the MR and making the improvements to the test documentation. Hopefully, it will help the next person to work on this.
There are no unanswered questions. Leaving at RTBC.
Comment #62
larowlanUpdating issue credits
Left some comments on the MR. I think we should be using the new Symfony API, not inventing our own protection
Comment #63
piotr pakulskitrying to update the code as suggested in the MR
Comment #65
prudloff commentedRebased and applied suggestions.
Comment #66
smustgrave commentedResolved the threads and believe all feedback has been addressed.
Comment #67
catchLooks like there is still an unresolved thread on the MR.
Comment #68
feyp commentedThanks @catch for looking at this! I think the open thread you're referring to is from an earlier review I did? This thread had been addressed by the time I wrote #60, also confirmed by another review by @quietone in #61. I would have resolved it at the time, but even though I have push access and I am the original author of the thread, I didn't have permission to resolve the thread and I still don't have permission to do so, unfortunately. I know this is kind of confusing, but imo the thread can be considered resolved.
Setting back to RTBC per #66.
Comment #70
longwaveCommitted a simplification, but added some comments about additional test cases.
Comment #71
smustgrave commentedNW for the open threads.
Comment #73
piotr pakulskiThe fix from this MR is already in Drupal 11 core. It was probably added during the Symfony 7 upgrade.
In the current D11 core, PagerParameters.php already has both changes:
getPagerQuery() now uses array_map('intval', ...) to safely handle the page values.
getPagerParameter() now catches BadRequestException when someone sends page[]=1 instead of page=1.
The only small difference is that the MR uses get('page', '') while core uses get('page') ?? '', but the result is the same.
This MR is 1357 commits behind main and the pipeline is failing. Since the problem is already fixed in D11, I think we can close this MR. If a backport to 10.x is still needed, it would require a new MR against the 10.x branch.