Remix 'Data Writes' go back to the basics of the web when it comes to form submissions. But therein lies a trap if you're not careful - Cross site request forgery (CSRF) can rear its head. In this session we'll explore what CSRF is. How it can be exploited and what you can do to make sure your Remix application isn't vulnerable.
Avoiding CSRF with Remix
AI Generated Video Summary
Welcome to my session on avoiding CSRF with Remix. Remix helps protect against CSRF by thinking about actions in terms of HTML form elements. To avoid CSRF with Remix, set the same site attribute on cookies and consider using a token in addition to the attribute. The token-based approach involves generating a unique token per session and form, transmitting it with the form, sending it back when the user submits the form, and securely validating it on the server side.
1. Introduction to CSRF and Remix
Welcome to my session on avoiding CSRF with Remix. CSRF stands for Cross-Site Request Forgery, where the attacker tricks the user into accessing a URL to perform an action using their existing session. Even if using post, an exploit can be achieved with an iframe and a separate URL. Remix helps protect against CSRF by thinking about actions in terms of HTML form elements. To avoid CSRF with Remix, set the same site attribute on cookies and consider using a token in addition to the attribute.
Hi and welcome to my session on avoiding CSRF with Remix. This is a lightning talk, so we're going to go through this in a pretty whirlwind fashion. So let's start up by talking about what CSRF is. Like most security exploits, it's got an acronym and it stands for Cross-Site Request Forgery. And in this scenario, basically the attacker tricks the user into accessing the URL and then this URL will perform an action using their existing session.
So a simple example, or perhaps the worst example, is an image with a URL and that URL performs an action. So before you say, no, I want to write code like this, I've seen this in apps that I've audited, and obviously the first thing the user is doing wrong here is they're using get to perform some sort of action. But equally, this kind of exploit can be achieved even if it's using post. And we do that by having an iframe with a hidden height and width, and that embeds a separate URL. And in that URL we have the form, the post to that endpoint, and we have an onload function that submits it the second the form is loaded.
So let's have a look at a simple Remix form example. We've got a primitive form here with a text field and a Submit button. We've got some sort of loader to check the user has got authenticated session because Cross-Site Request Forgery does require these to be logged in. You know, you're performing an action using their existing session that they didn't intend to do. And so we're not going to go into the details here, but yeah, the loader is making sure the user's got a session. We've then got an action in our component that does some sort of write to the database or similar. Something that performs some sort of action that, you know, can't be reversed. So how do you avoid Cross-Site Request Forgery with Remix in this scenario? Well, the first thing you should do is set the same site attribute on your cookies. So you can set that to lax, which will mean the browser will only send the cookies if it's a get request coming from another domain. Or you can set it to strict, which means it won't send any cookies for any requests that originate from another domain. So you might need to check whether you're using some sort of authentication flow that redirects to other sites as to which is the most appropriate for your site. But unfortunately, according to OWASP, this isn't enough and we shouldn't replace having a token. And the main reason for that is that only 93% of browsers support the same site attribute at this stage. We're almost there.
2. CSRF Token Generation and Validation
The token-based approach involves a four-step process: generating a unique token per session and form, transmitting it with the form, sending it back when the user submits the form, and securely validating it on the server side. To generate the token, secure random keys are generated, including a hash salt and a private key. The token is transmitted with the loader and returned in the loader data, which can be accessed in the form. Validation is done by regenerating the token and comparing it to the submitted version. If they don't match, an error is thrown.
Your app might have higher depending on your user's browser mix. So the token-based approach is commonly called synchronize the token pattern and it's basically a four step process.
The first thing is you have to generate a unique token per session, per form. You have to transmit that with the form. You have to send it back when the user submits the form and then you have to securely validate it on the server side.
So we start by generating some secure random keys. We generate a hash salt and this is something that you shouldn't store in your database. It should be on disk or environment variables so that if your database is compromised even then the hash salt is not leaked. You should also have a private key and both of these you can generate using the random bytes package from the crypto.
So the first step is to generate a unique random seed per session and so we do this by checking if the seed already exists in the session. If it does we use that. If it doesn't, we generate a new seed using again the random bytes function and we store that in the session. Then we need to generate a unique token for per session and identifier and in this scenario the identifier would be something unique for that form or operation and this prevents the same token being valid for two different operations and prevents token reuse.
So to do this we would take that hash, so we take our private key, we also take the seed and we combine the three together and we create a HMAC digest of that, which is basically a hash. It gives us a nice unique token. We have to transmit that token with the loader. The first thing we do is we generate the token and we return it in our loader data and that means we can then get that back in our form by using the route data. So we get our CRSF token from there and we transmit that in the form in our hidden field.
We then need to validate the code, so if we write a nice little utility function for validating it will regenerate the token based on the unique identifier and session and then it will compare the two using timing safe equals against the version that was submitted with the form. We use timing safe equals here to avoid timing attacks.
So how do we use this utility function? In our action, we can just call validate CRSF token using the value from the submitted form and if the two don't match then we throw an error.