- Issue created by @mahakjain
- Status changed to Postponed: needs info
7 months ago 4:38pm 12 June 2024 - Issue was unassigned.
Can you confirm the Drupal response is a 4xx HTTP error type? The network tab of the browser dev tools will show you that. If is is a 4xx type, Drupal is working properly here.
"A client error happened" seems to accompany 4xx responses, which should not be cached. I am trying to reproduce this from a new Drupal install.
I am able to reproduce the client error, which is expected as this is a 400 error. All that is required is to set
/user/password?name[foo][0]=foobar
.I did the following:
- Built a Drupal 10.3.0-RC1 site on http://simplytest.me
- Browsed to
/user/password?name[foo][0]=foobar
. - I noted the "A client error happened" error, and checked that it is a 400 HTTP response.
- Browsed to Browsed to
/user/password
, which works normally. So the response is not cached in a new Drupal installation.
There must be more steps to reproduce, or I misunderstand the bug report. Please list the exact steps to set up a Drupal site with this behavior, and put those in the issue summary.
- Status changed to Active
7 months ago 12:19pm 13 June 2024 - ๐ฎ๐ณIndia sourav_paul Kolkata
@cilefen I've checked that issue & successfully reproduced it in various Drupal versions - 10.1.8, 10.2.2, 10.2.6, 10.2.7.
Attaching SS for reference:
1. Hitting first time with malicious URL (after clearing the site cache)
2. hitting the right URL in second time (without clearing the cache)
Getting this warning on dblog (after hitting malicious URL for the first time)
I've noticed that not getting this warning in dblog for second time (even after hitting malicious URL so many times)
I've also checked some headers, where X-Drupal-Cache: HIT & Cache-Control: must-revalidate, no-cache, private .
Attaching SS of headers:
It seems like a caching issue...
I still can't reproduce this.
/user/password
is a 200 response, always. I've tested on two disparate platforms, including a new install. Both sites have the "Internal Page Cache" module enabled.I am most interested to see the response headers for the first error page. You didn't show those.
- ๐ฎ๐ณIndia sourav_paul Kolkata
@cilefen, Again I've tested it on fresh D10.2.2 with PHP 8.3
After clearing the cache, I hit the malicious URL, Sharing headers with dblog for the first error page...
headers SS:
dblog SS:
- Status changed to Postponed: needs info
7 months ago 10:23am 14 June 2024 Are there a lot of debugging options enabled? Those headers are not from a default install. What is the nginx configuration? The headers indicate this should not be cached.
- ๐บ๐ธUnited States karlshea Minneapolis ๐บ๐ธ
I got this on a site behind Fastly with the fastly module enabled if that might help, and clearing the caches fixed it.
I won't be able to really try and reproduce the issue for a couple of days but when I'm back if I find anything else I'll update the issue.
- ๐บ๐ธUnited States karlshea Minneapolis ๐บ๐ธ
I was able to reproduce behind Fastly, response is a 400 both from Fastly and hitting the app server directly.
Here's the maybe interesting part:
/user/password?name[foo][0]=foobar
does not trigger the page getting cached, but/user/password?name[%23post_render][0]=passthru
does. Maybe because?name[foo][0]=foobar
doesn't redirect? The latter redirects back to the page without any query parameters so I wonder if the redirect is triggering the cache.Response headers from Fastly when cached:
Accept-Ranges: bytes Alt-Svc: h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400 Cache-Control: no-store, no-cache, must-revalidate, max-age=0 Content-Language: en Content-Type: text/html; charset=UTF-8 Date: Mon, 24 Jun 2024 18:40:41 GMT Expires: Sun, 19 Nov 1978 05:00:00 GMT Strict-Transport-Security: max-age=300 X-Cache: MISS, MISS X-Cache-Hits: 0, 0 X-Content-Type-Options: nosniff X-Drupal-Dynamic-Cache: HIT X-Frame-Options: SAMEORIGIN X-Served-By: cache-dfw-kdfw8210044-DFW, cache-chi-klot8100063-CHI X-Timer: S1719254442.751705,VS0,VE95
Response headers directly from app server:
Cache-Control: must-revalidate, no-cache, private Connection: keep-alive Content-Language: en Content-Type: text/html; charset=UTF-8 Date: Mon, 24 Jun 2024 18:40:19 GMT Expires: Sun, 19 Nov 1978 05:00:00 GMT Fastly-Drupal-Html: YES Server: nginx Surrogate-Control: no-store, content="BigPipe/1.0" Surrogate-Key: eNqK igJ3 2tce mWTg juT3 F8oS PGx9 3F+3 grmK ojvY axoF eNI2 bpNa XtKW C6oq +QS4 pCAc Transfer-Encoding: chunked X-Content-Type-Options: nosniff X-Drupal-Dynamic-Cache: HIT X-Frame-Options: SAMEORIGIN X-Generator: Drupal 10 (https://www.drupal.org)
- ๐บ๐ธUnited States karlshea Minneapolis ๐บ๐ธ
/user/password?name[foo][0]=foobar
First request after cache clear:
HTTP/2 301 retry-after: 0 cache-control: max-age=86400 location: https://xxxx/user/password?name[foo][0]=foobar accept-ranges: bytes date: Mon, 24 Jun 2024 19:07:31 GMT x-served-by: cache-chi-klot8100127-CHI x-cache: HIT x-cache-hits: 0 x-timer: S1719256052.788341,VS0,VE0 strict-transport-security: max-age=300 content-length: 0 HTTP/2 400 content-type: text/html; charset=UTF-8 x-drupal-dynamic-cache: UNCACHEABLE content-language: en x-content-type-options: nosniff x-frame-options: SAMEORIGIN expires: Sun, 19 Nov 1978 05:00:00 GMT accept-ranges: bytes date: Mon, 24 Jun 2024 19:07:32 GMT x-served-by: cache-dfw-kdfw8210068-DFW, cache-chi-klot8100138-CHI x-cache: MISS, MISS x-cache-hits: 0, 0 x-timer: S1719256052.863280,VS0,VE366 cache-control: no-store, no-cache, must-revalidate, max-age=0 strict-transport-security: max-age=300 alt-svc: h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400
(This is not cached, hitting /user/password works fine)
/user/password?name[%23post_render][0]=passthru
First request after cache clear:
HTTP/2 301 content-type: text/html; charset=utf-8 location: https://xxxx/user/password x-drupal-route-normalizer: 1 content-language: en x-content-type-options: nosniff x-frame-options: SAMEORIGIN accept-ranges: bytes age: 0 date: Mon, 24 Jun 2024 19:11:16 GMT x-served-by: cache-dfw-kdfw8210117-DFW, cache-chi-klot8100046-CHI x-cache: MISS, MISS x-cache-hits: 0, 0 x-timer: S1719256276.102347,VS0,VE88 cache-control: no-store, no-cache, must-revalidate, max-age=0 strict-transport-security: max-age=300 alt-svc: h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400 content-length: 386 HTTP/2 400 content-type: text/html; charset=UTF-8 x-drupal-dynamic-cache: UNCACHEABLE content-language: en x-content-type-options: nosniff x-frame-options: SAMEORIGIN expires: Sun, 19 Nov 1978 05:00:00 GMT accept-ranges: bytes date: Mon, 24 Jun 2024 19:11:16 GMT x-served-by: cache-dfw-kdfw8210044-DFW, cache-chi-klot8100046-CHI x-cache: MISS, MISS x-cache-hits: 0, 0 x-timer: S1719256276.201582,VS0,VE385 cache-control: no-store, no-cache, must-revalidate, max-age=0 strict-transport-security: max-age=300 alt-svc: h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400
Request to /user/password afterwards:
HTTP/2 400 content-type: text/html; charset=UTF-8 x-drupal-dynamic-cache: HIT content-language: en x-content-type-options: nosniff x-frame-options: SAMEORIGIN expires: Sun, 19 Nov 1978 05:00:00 GMT accept-ranges: bytes date: Mon, 24 Jun 2024 19:12:33 GMT x-served-by: cache-dfw-kdfw8210044-DFW, cache-chi-klot8100170-CHI x-cache: MISS, MISS x-cache-hits: 0, 0 x-timer: S1719256353.239181,VS0,VE129 cache-control: no-store, no-cache, must-revalidate, max-age=0 strict-transport-security: max-age=300 alt-svc: h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400
Notice the Location header differences, that might be why one is getting cached.
- Status changed to Active
6 months ago 7:55am 5 July 2024 - ๐ญ๐บHungary mxr576 Hungary
It seems to me that enough information was provided in the previous comment to continue the investigation.
- Assigned to mxr576
- ๐ญ๐บHungary mxr576 Hungary
I also found this Symfony (non-Drupal!) HTTP exception in logs which could be related to the problem - in the past weeks I worked on caching issue and I remember seeing something related to how Symfony's own HTTP exceptions can cause problems with cacheability bubble up - like in this context they do no bubble up url.query_args cache context.
That is my hunch and I try to persuade that.
I guess the query string names actually matter for reproducing. Is that correct?
- ๐ฎ๐ณIndia sourav_paul Kolkata
yes @cilefen , you are correct...
It only occurred when we were using "name" query parameter..
- Issue was unassigned.
- ๐ญ๐บHungary mxr576 Hungary
Okay, this should be something else...
$form['name']['#default_value'] = $this->getRequest()->query->get('name');
fails in\Drupal\user\Form\UserPasswordForm::buildForm()
after/user/password?name[%23post_render][0]=passthru
was opened on
/user/password
because on that page the "current request" still contains thename
query parameter and it is an array, which triggers theInput value "name" contains a non-scalar value. (\Symfony\Component\HttpFoundation\Exception\BadRequestException)
.So how it could happen that a new request to
/user/password
still returns a query parameter from a previous request? - ๐ญ๐บHungary mxr576 Hungary
$form['name']['#default_value'] = \Drupal::service('request_stack')->query->get('name');
yield a different result, it fails because there is no query... so my assumption that
$this->requestStack
gets serialized and stored and resurrected.It also reveals another problem to me, the
user.pass/code> route must deny access when the <code>name
query parameter is missing, or handle it leniently. - ๐ญ๐บHungary mxr576 Hungary
\Symfony\Component\HttpKernel\EventListener\RouterListener::onKernelRequest() \Drupal\Core\Routing\AccessAwareRouter::matchRequest() \Drupal\Core\Routing\Router::matchRequest() \Drupal\Core\Routing\Router::getInitialRouteCollection() \Drupal\Core\Routing\RouteProvider::getRouteCollectionForRequest() \Drupal\Core\Routing\RouteProvider::getRouteCollectionCacheId() # DOES NOT CONSIDER QUERY PARAMETERS WHEN GENERATES A CID. \Drupal\Core\Routing\RouteProvider::getRouteCollectionForRequest() # modifies $request and overrides query parameters on, WHY??!?!
So this is how first a request to
/user/password?name[%23post_render][0]=passthru
can cause that a request touser/password
afterwards also has aname
query parameter... - ๐ญ๐บHungary mxr576 Hungary
\Drupal\Core\Routing\RouteProvider::getRouteCollectionCacheId()
generatesroute:[language]=en:/user/password:
as CID, notice thatname
query param is not included in the cid. - Merge request !8675Draft: Issue #3426324 by Wim Leers: ExistsConstraintValidator should ignore NULL... โ (Closed) created by mxr576
- Status changed to Needs work
6 months ago 12:40pm 5 July 2024 - ๐ญ๐บHungary mxr576 Hungary
Now I think we have repro steps, I even have a draft fix for the problem with a todo.
- ๐ญ๐บHungary mxr576 Hungary
@cilefen could be right in #10, Drupal 10.3.0+ could have a fix for that already...
https://git.drupalcode.org/project/drupal/-/commit/fbe023ba6ddbf719ee8d4...
- Status changed to Closed: outdated
6 months ago 2:06pm 5 July 2024 - ๐ญ๐บHungary mxr576 Hungary
Yep, 10.3.0 has the fix, so the suggested step is update from prior version or grab the fix from the referenced issue.
- ๐ง๐ชBelgium siemen_hermans
We encountered the exact same issue today on one of our websites that's currently running on Drupal core 10.3.1.
Clearing the cache however did resolve the issue.Sadly enough this does mean that the changes present in Drupal 10.3.1 do not resolve the issue.
- Status changed to Active
2 months ago 10:11am 8 November 2024 - ๐ฒ๐ฉMoldova marjina-constantin
I am using Drupal 10.3.5 and the error still occurs.
Steps to reproduce:- Using anonymous user, on cold cache, visit "/user/password?name[%23post_render][0]=passthru"
- "A client error happened" error message appears, which is expected.
- Visit "/user/password" without any query parameters and confirm that "A client error happened" still appears.
- ๐ฒ๐ฉMoldova marjina-constantin
After some investigation, I found that starting with Symfony 6.0, the
InputBag::get()
method now throws aBadRequestException
when attempting to retrieve non-scalar values. In our project, this affects theUserPasswordForm.php
file, whereInputBag::get()
is used to retrieve the default value for the "name" field. When this exception occurs, it gets cached and displayed to all subsequent anonymous requests.Symfony's documentation recommends using
InputBag::all()
to retrieve arrays instead. While we arenโt expecting an array for "name" here, switching from get to all should prevent issues with this specific form. The root cause may be related to a broader query parameter caching issue between requests, but this change will safeguard us in this particular context.Regarding security, this change looks safe. The
RequestSanitizer
class handles potentially unsafe keys in query parameters by removing them from the request object.Iโm attaching a patch with this adjustment for review.
- ๐ณ๐ฟNew Zealand quietone
Changes are made on on 11.x (our main development branch) first, and are then back ported as needed according to our policies.