Protecting Users Against Cross-Site Request Forgery
Product & Tech
Imagine this scenario: after logging into your bank account and checking your balance, you navigate to another website. Suddenly, you notice your account was hijacked! Funds are being transferred and the attacker tries to reset your password. You are security-conscious and never fall for phishing scams or share your password, so how could this have happened?
This could be an example of Cross-Site Request Forgery, often abbreviated as CSRF (or XSRF, if you are feeling eXtreme). XSRF is a common web-security exploit that even tech giants like Google, Netflix, and Microsoft have proven susceptible to in the past. Fortunately, Roblox has safeguards against XSRF, but let’s dig into how it works and what tools we have leveraged to protect our website.
You visit the website of an attacker, and that website makes your browser send requests on your behalf. They might manipulate your data, or have your browser perform unwanted actions for you, like resetting your password, changing your email, or purchasing a virtual item. Because your browser includes your cookies and session information with the request, the web server will trust the request and perform what is asked.
This is a flaw that has existed since the early days of the Internet. A number of solutions have been developed around validating the referrer or origin header, but they do not work in all cases. SameSite cookies help in many cases, but they’re not yet supported by all browsers and we need an additional layer of security.
When an HTTP request reaches the backend, the web server verifies that the XSRF token matches the expected value. If it does not, the request is rejected and the endpoint returns an appropriate error response. Note that the value is unique for each user, and changes over time to reduce the risk of replay attacks.
XSRF at Roblox
At Roblox, we historically applied XSRF protection on an opt-in basis, meaning that we had to manually tag each data-mutating endpoint with a C# attribute like [XsrfProtection]. This was a viable solution for Roblox for many years but is fundamentally insecure by default because it relies on engineers remembering to add a specific piece of code to every endpoint, which was not efficient. We also added code analysis tools that flagged the absence.
When we began work on a new RESTful web API framework in 2015, we added XSRF protection by default, with the option to opt-out for specific endpoints. This worked flawlessly, but we still had to tackle the problem of adding this protection to existing endpoints.
- Find a way to introduce XSRF protection across the board for all existing endpoints.
- Solution should not require any customization on a per-endpoint basis.
- Solution should have minimal or no impact to production users.
One challenge is that we need to support all of our front-end frameworks:
- React (primary framework, used with Axios for HTTP calls)
- ASP.Net MVC
- ASP.Net WebForms
- Native C++ code (used for HTTP calls from the Roblox Game Client)
This is where it gets a little tricky. Some of our legacy pages on the website rely on using regular form browser posts, where the browser is in charge of making the request. A browser post doesn’t support attaching headers, and it also doesn’t support retrying failed requests that pass in an old token.
For ASP.Net MVC endpoints, we have to worry about two different use cases:
- AJAX requests
- Browser form posts
Any MVC endpoint can be accessed either way, which means we have to adapt our backend validation to look for the XSRF token in both the headers and the form body.
ASP.Net’s built-in approach for browser form posts is to use a [ValidateAntiForgeryToken] attribute, but it has to be manually added to each endpoint. Additionally, it doesn’t support AJAX requests unless the requests are happening on a page that embeds the anti-forgery token.
Part 1: Automatically attach XSRF as a header
- React – axios.interceptors.request.use
- AngularJS – httpProvider.interceptors.push
- jQuery – $.ajaxPrefilter
Part 2: Automatically inject XSRF token in form posts
This can be done by overriding the behavior of HTMLFormElement.prototype.submit with a function that adds an <input name=”CsrfToken” value=”secret”> to the form before submitting.
What about cases where the user has left a page idle for a few minutes and tries to submit a form after the XSRF token has already expired? For AJAX requests, this is no big deal, we can simply retry the request. However, with form posts, the browser controls the page flow and we can’t gracefully handle this scenario. We solved this by exposing an endpoint that returned the XSRF token in the response headers when asked by the client. If the user tries to submit a form and we detect that the token is stale, based on the original page load time, we cancel the form submission, and ask for the latest XSRF token to ensure that we get a valid one. Only when we get that latest token do we proceed with re-submitting the form.
For our backend implementation, we created a XsrfValidationFilterAttribute action filter attribute that we registered at a base-level for all of our websites. This class runs before each endpoint is executed, and verifies that XSRF token is present for all data-mutating endpoints (based on whether the HTTP method is POST, PUT, PATCH, or DELETE).
When adding new security features, measure the impact before you start enforcing.
Adding per-endpoint metrics to tell us which URLs did not gracefully handle XSRF proved invaluable when debugging issues. We used the same approach before enabling Content-Security-Policy.
When an HTML element contains an input with a name attribute, that takes precedence over any properties with the same name.
<input name=”action” value=””>
One of the pages on our website had a form input named ‘action’, so our XSRF code which called form.action was inadvertently reading the input value instead of the form property!
Thankfully our quality assurance testers spotted the issue early on and switching to form.getAttribute(“action”) solved the issue.
Neither Roblox Corporation nor this blog endorses or supports any company or service. Also, no guarantees or promises are made regarding the accuracy, reliability or completeness of the information contained in this blog.
This blog post was originally published on the Roblox Tech Blog.