CSRF checks fail with a standard EZproxy instance

TL;DR — with a standard config, EZproxy filters out headers with the names X-CSRF-TOKEN and X-XSRF-TOKEN. Any CSRF validation checks that require one of those headers to be present in a request will fail.

A woman looking perplexed as her CSRF headers disappear in to the void
Photo by Annie Spratt on Unsplash

What’s going on

I wanted to write something about this because my googling for the terms CSRF and EZproxy returned nothing of any use, so maybe this will help someone.

I’ve been investigating a bug where users visiting a Laravel-based site via EZproxy were getting errors when trying to do various actions. It turned out they were getting 419 errors, which is Laravel’s non-standard response denoting a CSRF validation failure.

On this particular project we’ve had our fair share of CSRF issues, mostly caused by the fact that the standard way of doing CSRF checks is to write a token out into the page as a hidden form field. This can be a problem if the page in question can be served from a CDN cache, like Cloudfront or Cloudflare, because it means User A can receive a page with User B’s CSRF token in it. The ways around that issue are either to use javascript to get hold of the token another way — i.e from the XSRF-TOKEN cookie that Laravel also creates for you — and append that as a hidden field to the form or (as a last resort) to just exclude the endpoint in question from CSRF checks. See https://blog.cloudflare.com/the-curious-case-of-caching-csrf-tokens/ and https://www.fastly.com/blog/caching-uncacheable-csrf-security for discussion about that. Note that while this case involved a Laravel server, Spring Boot, Play, Express and other common web frameworks do very similar things.

In this case, the caching issue was ruled out by the fact that these particular actions were working fine for users visiting the site directly but failing with a 419 for proxy users. My suspicion, after playing around with a few different ways of sending the tokens, was that they must be getting filtered out before reaching our server.

To try and confirm this, I deployed a small investigatory change to our dev server because I couldn’t access my own locally hosted version of the server via the proxy. First I excluded one of the affected endpoints from the CSRF checks, as described in laravel’s docs, and then I logged the values of the X-CSRF-TOKEN and X-XSRF-TOKEN headers within the controller method for the route. Then I accessed that endpoint both with and without a proxy.

The logging showed that those headers were populated when accessed directly but empty when accessed via the proxy.

So what can you do

EZproxy provides configuration options for how it deals with headers — described here — and those let you nominate specific headers to be passed through unaltered.

The problem is that if you’re the owner of the server being visited, you don’t have any control over how somebody visiting your site has configured their proxy, and as far as I can tell those headers are filtered out by the out-of-the-box EZproxy stanza.

So I guess you have three options:

Whether option 3 is acceptable or not will depend on your project, but it’s certainly the easiest!

If anyone is aware of other options, or if anyone has a specific workaround for this issue, I’d love to hear them.

EDIT: I discuss my approach to option 2 — i.e. working around the problem without disabling security — in this article.