All,
I've been playing with this Filter recently, and I have some concerns
for its practical use. I'm considering adding some features to it in
order to make it more practical to use, and I'm interested to see what
others think about these "problems" and whether my potential additions
have merit.
These are in no particular order (e.g. importance).
1. Killing caching
If used as prescribed, all URLs will have CSRF tokens added to them even
when they are unnecessary. Specifically, I'm thinking about static
resources such as images, scripts, and CSS.
A sample page I'm using for testing uses a standard template for the web
application I'm working with, and it normally requests 3 CSS files, 6
Javascript files, and a site-logo. The browser very much likes to use
the "favicons" that are defined in the page, and so it (Firefox) will
load 2 different sizes of favicon for various purposes.
Simply clicking into the title bar and pressing ENTER shows that /all of
those resources are re-fetched from the server for every request/. The
browser cache has been defeated.
Each of these resources has been configured to return the following HTTP
response headers to encourage caching:
Cache-Control: public
ETag: [the etag, which is constant]
The same of course happens if you perform a "quick" page reload (i.e.
CTRL-R/CMD-R/click-reload-button) rather than performing a FULL page
reload (i.e. SHITF-CTRL-R/SHIFT-CMD-R/SHIFT-click-reload-button).
This happens because every page-access generates a new CSRF token for
that request which is used for all the URL references on that page.
Intermediate caches such as true caching HTTP servers will likely have
the same problem. For caching HTTP servers, this represents a mild DOS
attack since those caches will be wasting their cache space on one-time
URLs. They could surely be tuned to ignore query-strings for certain URL
patterns, but then cache performance (i.e. speed) will be decreased due
to that URL-processing being added to the pipeline.
I think it might have some sense to add some filtering to the
CsrfResponseWrapper (which is where HttpServletResponse.encodeURL and
HttpServetRequest.encoreRedirectURL are hooked to add these token
parameters to the URLs) to allow it to STOP adding tokens to certain URL
patterns. Prefix (/images/*) or suffix (*.css) matching seems to make
sense to me for inexpensive checks to get caching working again.
2. <form>s with either GET or POST. Or, really... "Forms"
When using <form method="get" action="/foo?csrf=blahblah">, different
browsers do different things with the query string. I'm mostly testing
on Firefox and I don't have time to check this on every browser. It's
enough that Firefox behaves in this way.
<form method="get" action="/foo?csrf=blahblah">
<input type="submit" name="btn" value="Go!" />
</form>
Submitting this form causes a 403 response. The browser shows that the
URL requested from the server is /foo?btn=Go! and the CSRF token has
been removed from the action. (Surprise!) I don't see anything in
WHATWG's description of how form-submission is supposed to work that
indicates that GET versus POST should behave differently with respect to
the 'action' attribute, but this is what the current version of Firefox
actually does.
There are three options I can think of to get around this:
a. Switch to POST:
<form method="post" action="/foo?csrf=blahblah">
<input type="submit" name="btn" value="Go!" />
</form>
You get HTTP POST /foo?csrf=blahblah with an
application/x-www-urlencoded entity containing the form <input> element(s).
b. Manually add the CSRF token to the form:
<form method="get" action="/foo?csrf=blahblah">
[ if
request.getAttribute("org.apache.catalina.filters.CSRF_NONCE_PARAM_NAME") ]
<input type="hidden" name="csrf" value="[
request.getAttribute("org.apache.catalina.filters.CSRF_NONCE_PARAM_NAME")
]" />
[ end-if]
<input type="submit" name="btn" value="Go!" />
</form>
In this case, you get a GET request with the csrf parameter in there
because, well, you asked for it.
c. Hack the CsrfPreventionFilter to allow all POST methods. This is one
of my proposals: allow users to configure the CSRF prevention filter to
always-allow HTTP POSTs. This cannot be done in web.xml using e.g.
<http-method>GET</http-method> for the <filter> because Filters cannot
be configured in this way. And if they could be, the Filter would not be
invoked and all the URLs on your page would lack the required CSRF
tokens to make /those/ links work.
3. Default token-cache size
The default token cache size is 5. If you use applications like I do,
you often open multiple links into separate tabs for various reasons. Do
a search of products in your application, and there are several of them
you'd like to get details for. So you middle-click (or whatever does it
in your environment) on each of those links to open separate tabs,
leaving the search-results page visible in the current tab.
Then you want to click the "next" button to view the next page of the
results. An HTTP 403 response is returned because, if you opened more
than 4 extra tabs, the tokens used on the search-results page have
already expired, and you can't use your page anymore.
I'm not sure what the best token cache size is. It might depend upon the
behavior of your users. If I use your site, you might want a very large
token cache size. But I think maybe 5 isn't quite big enough. At least
it's trivially settable, so I don't really have a recommendation here
other than "maybe let's set the default to something higher"?
4. No "test mode"
It might be really helpful to have a non-enforcement mode that generates
logging without any errors. This would allow developers to enable the
CSRF prevention filter in a mode where the nonces are generated and
used, but only checked and logged. Any time a token would have been
rejected, it is logged but the request is allowed.
The logging is already there, but requests are rejected.
I am having a problem at $work where anytime I enable this in a
development environment, I get loads of complaints from users being
locked-out of things that I haven't had the time to test and/or mitigate
(such as <form> submissions). If there were a non-enforcement mode, I
could enable it, then look at the logs after a week and fix anything I
find in there without disturbing anybody.
5. All or nothing
This may be 4(b) instead of "5", but...
When enabling the CSRF prevention filter, it's impractical to configure
it for anything but <url-pattern>/*</url-pattern>. If you choose any
other filter-pattern, you will find that you need numbers of "entry
points" which are request URLs that are allowed to be requested
/without/ providing a CSRF token.
This means that your *entire application* needs to be working perfectly
in order to enable the CSRF prevention filter.
It would be nice to allow enforcement of, for example, subsets of the
URL space. Maybe I only want to enforce the CSRF tokens in my /admin/
and /product/ URL spaces, allowing the /public/ and /whatever/ spaces to
remain unprotected.
But if I visit /public/index and want to click on a link pointing to
/admin/index, it's got to have that CSRF token in it otherwise I get an
error. So I can't just use <url-pattern>/product/*</url-pattern> and
<url-pattern>/product/</url-pattern> because I will not only get
non-enforcement for those other URLs, I will also get no tokens.
Providing one or more non-enforcement URL spaces (prefixes?) would allow
users to enable CSRF mitigations on parts of their web applications,
test and fix anything that isn't working, then move on to other parts of
the application, adding enforcement a chunk at a time.
I'm not sure what others' experiences od using the CSRF prevention
filter have been like, but my experience so far has been very rocky as
you can see from the issues I'm raising above.
I'd be very happy to receive feedback on anything I've written above.
-chris
[1] https://html.spec.whatwg.org/#concept-form-submit
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org