Join Kevin Skoglund for an in-depth discussion in this video Cross-site request forgery (CSRF), part of Creating Secure PHP Websites.
In this movie, we'll learn how to use PHP to protect against cross-site request forgery, which is also known as CSRF. Cross-site request forgery is when a hacker tricks a user into making a request to a third party website, presumably your website. Andy they can do that simply to generate fraudulent clicks or fraudulent requests, requests that the user did not intend to make. But even more concerning, they can do it to take advantage of a user's logged-in state. Let's take a look at a quick example from the user's point of view.
Imagine that you, as a user, are logged into your bank account. And once you're done with your banking, you navigate away from the window, but you don't actually click logout. So, your session with the bank is still open, the bank doesn't know that you've surfed somewhere else. As far as they're concerned you may be about to click another link at any moment to perform another action. So, you begin to surf the Internet instead. And you load a new web page which contains a specially crafted image, and that image contains a source which is not to an image file but instead, is a URL request to your bank to transfer money from one account to another.
You can see why this would be a problem. If you're not logged in, this would just be a request on your behalf to another server that you didn't intend. But if you're logged in, then it's a request as a logged in user to that server which is potentially more dangerous and it doesn't have to be your bank, it could be PayPal, Amazon.com, eBay, or any site that you might create. So, how do we defend against CSRF? The first thing is that GET requests should always be idempotent. Idempotent means that they make no changes, that they can be called repeatedly over and over and over again.
We only want to use POST requests whenever we're making changes. Typically a GET request is what happens when you have a link or URL. And a POST request is from a web form. If you follow this division, then GET requests always become reading operations, just reading data from the server, while POST requests can potentially write to the server. Why does the request method matter? Well, because those image source URLs are always GET requests. It's possible to generate a POST request without actually having a web form, but it's very hard to trick a third party into sending a POST request without having one.
So, that's step one. Check the request method. We'll see how to do that in PHP in a moment. Step two is to ensure that the form data that we receive comes from legitimate forms that we generated for the users, not from a faked version of the form. We can do that by storing a form token in the user's session, and then we can add a hidden field to the form that includes that token value. When the form comes back to us, we'll get that token value sent back with their form data and we compare the form token that we have stored in the session with the one that just came in with the form.
If they match, then we know that we generated this form for the user. We can take the additional step of checking the token generation time to make sure that it was created recently. That way, even if someone copies the source code for your form, it will expire after a short period of time. Let's take a look at how we can do this in PHP. Included in the exercise files there are three files related to CSRF, CSRF request type functions is the first one we'll look at. If you open this up you'll see there's two functions, one called request_is_get and one that is request_is_post, these will simply just check to see whether the request method that was submitted is either get or post.
There are other types of request methods, like put. And if you wanted to add additional functions, you could. But these are by far and away the two most common ones, get and post. This will then allow us to check and see. If it's a post request, we can perform write operations and do destructive things. But if it's a get request, then we won't. And the get request, we will only allow them to read back information. It'll be idempotent. The second set of functions is in this csrf token functions file. If we open that file up, you'll see that there are a number of functions related to this.
The first one, csrf_token, is just going to generate a unique token that we can use for our CSRF protection. That's all it does. Just returns a string that's going to be used as our token. You could craft that a number of different ways. This is one of the common ways to do it. The second function create_csrf_token, will create that token and then store it in the session. We also have a destroy_csrf_token that does the opposite, it simply wipes it out of the session. Then the real work is going to be done here by csrf_token_tag.
The idea is that we can use this is in our form to create an HTML tag for the form with the CSRF token. So, it'll create the token for us, and then drop it into a bed of HTML. Once we've done that, then that token will be generated, stored in the user's session, and also present on the web form. When they submit the value back to us, we just have to check and see if the token is valid, and we do that with csrf_token_is_valid. So, we check to see if the post request included that token. If it didn't, then we return false.
If it did, then we'll compare it. We'll compare the user token with the stored token, and if they match, we'll know that it's a valid form. What you do with that information is up to you. You can handle it a number of different ways. One way is to simply tell it to die on token failure. I usually prefer to handle it in a custom way, rather than a stop everything on failure kind of way. And next we have an optional check for csrf_token_is_recent. That will take the current session time that we stored when we generated the token and we'll compare it against the current time to see if a maximum elapsed time has passed or not.
Here I've set it to just simply be one day. So if it's been less than one day, then the token is still considered valid. After one day, we will destroy the token and return false, and say that it is not recent. We can see how we use all of these functions in this file, csrf_demo.php. If you open that up, you'll see that we start the session, we're going to need that session to be able to store the token in the session and then we were going to require those two sets of function files. After we do that, we're going to check to see if the request going in is a POST request.
The first time we load the page, it's probably not a POST request. It's probably a GET request. So, instead, it'll just have a message that says Please login, and it'll display some HTML down here. Notice that on that on that page, it's going to echo csrf_token_tag. That's going to generate the tag, store in the session, and output an HTML tag right here on our form. Let's take a look at that. Go to Firefox, you can see I've already got the page open, csrf_demo.php. We load the page for the first time, it's a get request and we see, Please login.
If you view the page source, then you'll see that there is a csrf_token here and this is the value for it. That is going to be sent in whenever we submit the form. Is a hidden field so we don't actually see it. Now, when it gets submitted. This time, it will be a POST request, and so we'll check to see if the token is valid. If it is valid, the message will be VALID_FORM_SUBMISSION and then it will check to see if it's recent, and display a message saying, either it's recent, or not recent. If the token is not valid, then it will tell us that it's either missing or mismatched, as a simple message.
Let's check out this demonstration, see how it works. So here, as we already saw, we have a get request, we see Please login, and we click Submit. It says, yes it's a valid form submission, and it's recent. It knows that it came from this form. One way that we can test that is by removing the token. The form came from somewhere else, and someone faked our form, but they did not include the token, then now you can see it says, token is missing or mismatch. So we have to have that CSRF token there, or our code will not process this request.
I've tried to package up these CSRF functions into two files that you can simply include into your webpage or into your web application. If you only allow post requests to make changes, and you generate tokens to protect your web forms, then CSRF will no longer be a threat
- Cross-site scripting (XSS)
- Cross-site request forgery (CSRF)
- SQL injection
- Encrypting and signing cookies
- Session hijacking and fixation
- Securing uploaded files
- User authentication
- Throttling brute-force attacks
- Blacklisting IPs
- Implementing password reset tokens