Hijacking the Key(s) to the (Magento) Kingdom
Over the last few years, Magento has gradually increased protection against CSRF attacks. The most common defence against such attacks is to require a form key (a randomly generated string, unique to the session) to be submitted with all actions that perform update/insert commands on the server. In version 18.104.22.168 this protection was implemented for the add to bag action. The problem with this was that it introduced ‘personalised’ content to some of the highest trafficked pages on the site, the product view pages. The reason this is a problem is the implications this has on any FPC implementation you may have in place. In this blog post, I’m going to review an issue with the approach some developers have taken to resolve this issue.
So we now have a piece of content that needs to be different for every user on every single product page. For some sites, this won’t be a new issue to deal with as the default recently viewed block is also ‘personalised’ content. Because of this, some FPC extensions already have specific ways of dealing with such content. Whether this is achieved via varnish ESI, lazy loading via AJAX, injection from a cookie or some other method will vary based on the structure of the extension. If your extension implements any of these approaches then this article is not aimed at you.
This article is aimed at the developers that decided “adding an item to the cart isn’t exactly the most sensitive functionality ever, we’ll just disable the check”. I don’t really disagree with this sentiment. The thing that I’m concerned with, is the way that this is implemented. I’ve seen several approaches that actually enable more serious attacks to be performed. If you decide this action doesn’t require a form key, that’s fine. But there are absolutely actions which do (such as changing account details) and because of this, the secrecy of the key is important.
One approach that I’ve seen both in the wild, and recommended in the issues section of GitHub repos is to do something like the following:
public function bypassFormKeyValidation($observer)
$key = Mage::getSingleton('core/session')->getFormKey();
This essentially means that regardless of whether the form_key is submitted in the request, it will be copied from the session into the request object. Therefore, at the point it’s checked, it will always be valid. Because a unique value no longer needs to be submitted, the page can be cached as normal. In itself, this isn’t the worst approach in the world, but there is one additional step, that appears to be often overlooked, but is extremely important. Remove the form_key from being rendered on the cached page.
So why is this so important? Because if a form key is stored in your FPC, it can potentially be retrieved by malicious actors. Imagine the following scenario. You’re busy shopping away on target.com and you decide to see if there are any promotion codes floating around in the wild. You perform a Google search and the top result is evil.com, which is saying there’s a 30% discount code available, great!! So you click the link.
If you’re already logged into target.com at this point, you’re in trouble. The page you view on evil.com can force your browser to request a unique product URL on target.com. It can do this either by opening an iframe or in the case of X-Frame-Option restrictions, a pop-under. Due to the generic handling of request params, creating unique URLs for the same page in Magento is pretty easy, this means it should be easy to generate a unique un-cached URL. At this point, your form_key is going to be cached with a cache key known to evil.com. The Same Origin Policy prevents evil.com from using JS to fetch this data directly from target.com, so instead, it simply makes an AJAX request to evil.com, which will then curl target.com, scrape out the form_key and return it as a response to the AJAX request. Evil.com can then generate an HTML form, complete with your form_key and submit it, performing CSRF attacks against you. If target.com is using a version of Magento prior to 1.9.3, this can lead to complete account take over.
Moral of the story, guard those keys with care!