As summarised by OWASP, Cross-Site Request Forgery (CSRF) is “an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated”.
There are several recommended ways to defend against CSRF and the strongest, and very popular, mechanism is the Synchronizer Token. This technique requires a secure, random token to be included as an extra hidden field to be sent with requests. When the server receives the request it compares the token with a value stored in the server-side session state corresponding to the user’s session cookie also sent in the request. If the comparison is a match the request is processed, otherwise the request is rejected as fraudulent.
You cannot cache it
The Synchronizer Token is very effective but it also happens to be the only recommended CSRF defense that is dependent on server-side state and session cookies. This dependence has a negative impact on cacheability.
Take a common ecommerce site example where the home page lists the most popular products and a convenient
Add to cart button beside each product. When the button is clicked, a request is made that needs to include the anti-CSRF Synchronizer Token specific to the active user in order for the request to succeed.
Typically the token would be embedded in the HTML form’s hidden input fields which means that this high-traffic page would need to be rendered by the origin web server individually for each user. This means it cannot be served from a shared CDN cache.
Furthermore, every user who loads the home page - including an unauthenticated, first-time visitor - needs to be assigned a server-side session. That is potentially a large quantity of traffic and workload hitting the web servers and increasing page load times for the end users.
This impact isn’t only limited to a home page but also category pages, search results pages, and the individual product pages - wherever the add-to-cart functionality is protected against cross-site request forgery.
How did we get here?
Web security is a complex field and it is this complexity that often leads to misunderstanding and mistakes and ultimately to producing vulnerable software. One way this is tackled is by trying to remove to need to consider the complexity at all.
Programming languages and frameworks have tried to pre-empt common security vulnerabilities by implementing many recommended mitigations in the core behaviour and in the default project scaffolding. This is certainly a great step toward reducing the number of security problems in software in general but it can interfere when secure-by-default is trading off other important criteria - like scalability or performance.
The industry has also built automated security scanning tools that will flag almost any HTTP POST method as requiring CSRF mitigation, and every cookie as needing the “httponly” attribute, partly because a tool cannot easily determine the real purpose of each request or cookie and errs toward a conservative view. On top of existing pressures to deliver value, people in the IT business often lack the knowledge, or the confidence, or even the appetite to argue each case that the tools have highlighted in a security report, and instead comply by implementing a blanket mitigation.
In the context of CSRF, it is important to revisit the intent of the requests you’re trying to protect. Continuing the original example, it is debatable whether there is any significant impact if an unwanted product could be added to a visitor’s cart, assuming that the contents of the cart are displayed to the user for review before beginning the checkout process. In many situations forging this request would have negligible consequences.
However, allowing an unintended add-to-cart action could be a problem for products of a sensitive nature, or it could be a problem if adding an item to a cart is sufficient to alter a user’s list of “other products you may like” and future marketing campaigns. This is unfortunately where the complexity appears and the details of your specific site will need to be considered to make the right call.
In some cases, if the platform your website is built upon only has limited controls, you may need to disable the CSRF protection on certain actions to improve the cachability of your pages and give your users a faster experience and to enable your origin to cope with greater volumes of traffic.
Alternatively you may be able to use Edge Side Includes (ESI) or AJAX to update the CSRF token in the forms to match the user’s session. This can enable most of the page to be served from cache but will still require (hopefully simpler) requests back to the origin to fetch the token. This is how Turpentine solves the problem for Magento 1.x.
With a programmable caching solution, like Varnish Cache and its VCL, you could substitute Synchronizer Tokens with another CSRF protection mechanism which does not depend on session state. As a start, performing Referer and Origin request header checks is easily done in Varnish and it is a recommended practice anyway. Most of the Double Submit Cookie technique can be implemented in Varnish with the origin performing the final check. If changing the origin is infeasible, Varnish Modules (VMODs) can provide the missing pieces to implement a Double Submit Cookie or even the Encrypted Token Pattern completely in VCL.
While all CSRF prevention techniques are vulnerable (or irrelevant) to cross-site scripting (XSS) and man-in-the-middle (MITM) attacks, the stateless mechanisms can also be vulnerable to sub-domain attack vectors. So if your www.example.com site also has a blog.example.com site, any XSS vulnerabilities on the blog could be used bypass CSRF protection on “www”.
Most POST actions on your website, like login, checkout, and change-password can retain the stronger Synchronizer Tokens, and the alternative protection types could be limited for use with lower-risk actions like add-to-cart.
It is also important to ensure you have other security mechanisms in place to mitigate more than just CSRF attacks. This includes serving an appropriate
Content-Security-Policy response header to mitigate Clickjacking and ensuring that HTTPS is used for all subsequent requests once a user has authenticated, or better yet use HTTPS-only for the entire site.