Description
We currently do not have any built-in means to protect Symfony apps from flooding gateway caches such as Varnish and the like. Basically, given I have configured a route for /about-us
and I send caching headers such as Cache-Control: public, max-age=86400
, one can easily flood any cache by just adding random query parameters to the URL. So a simple script that calls /about-us?<random-key>=<random-value>
floods the cache with nonsense entries.
The only way to prevent that is to deny such requests by sending e.g. a 400 Bad Request
(well, any uncacheable status code but 400 would be appropriate here imho).
Now, of course you can do that manually today by having e.g. a kernel.request
listener that checks for valid query parameters. You could even configure it on proxy level.
But those "solutions" are all cumbersome for many reasons:
-
Configuring it at proxy level requires you to specify a whitelist of allowed query parameters. Either a global one or even different parameters per URL/route. As soon as you deploy a new version of your application, you might also need to add new parameters to your whitelist. It's cumbersome and error-prone. Moreover, once you switch to a different cache proxy, you have to start all over again.
-
Building your own solution on app level using e.g. the
kernel.request
event is still cumbersome and error-prone because again, you have to manage a whitelist and there's no standard way of doing things which means that in application A one might have implemented it with a global whitelist and in application B someone implemented it as a route attribute. It's a mess.
I was thinking about this issue and I was wondering if we can come up with a general solution. A solution that protects all Symfony applications by default and in best case, doesn't even require any developer to think about a whitelist at all.
So here's an idea I came up with and I would like to know if that's something people would love to see being integrated into Symfony or if there's even better ideas:
Symfony already tracks certain things during a typical request flow. The best example is security/session access. Symfony automatically turns any responses into private
responses if any of the code has accessed the current user and thus prevents responses to be cached and potentially leak private information to other visitors.
We could do something similar with query parameters. We could extend our ParameterBag
to track calls to the get()
and has()
methods and introduce a method which would return all the query parameters that haven't been accessed (or the other way around - implementation detail).
Then we use a kernel.response
listener with a fairly low priority that checks if there were any query parameters on the master request that were never accessed during this request in which case it would send a 400 Bad Request
.
That means, you don't have to configure anything at all. It all works out of the box and you will not even notice this logic if you behave normally and do not send random query parameters 😎
Things to consider:
-
It may require changes in bundles. If a bundle uses
$request->query->all()
instead of specific calls, you may suddenly get 400 responses. Thus, for BC reasons, this listener should be disabled by default in Symfony 5. It may be enabled by default in Symfony 6. -
We may think about having a special
QueryParameterBag
that does not provide anall()
method because with this concept it never makes sense to access all parameters that were submitted. We may introduce amultiple(array $keys)
though for DX. -
The response listener doesn't need to do anything, if there's no caching information present on the response.
What do you think?
/cc'ing people that I know worked a lot with caching in Symfony in e.g FOSHttpCache or API Platform: @dbu @andrerom @dunglas @alexander-schranz