Creating Secure AJAX HTML Forms in ASP.NET Core MVC, Part 2: Preventing Cross-Site Request Forgery Attacks
In Part 1 of this two-part series, I showed you how to secure HTML forms from XSS and SQL Injection attacks by implementing client-side and server-side validation. In Part 2, I will show you how to build an HTML form that submits using AJAX and how to protect it from Cross-Site Request Forgery attacks.
HTML forms are one of the most common ways for a web application to accept user input. However, in this modern age, it is best practice not to reload the page whenever an HTML form is submitted. AJAX is one tool we can use to submit a form and provide feedback to the user without reloading a page. Implementing your application this way comes with inherent security concerns. One of those concerns is Cross-Site Request Forgery Attacks. This type of attack can be carried out on virtually any HTML form. In this blog, I will show you how to create an HTML form that submits using AJAX, and how to protect it from Cross-Site Request Forgery attacks.
Cross-Site Request Forgery Attack
Cross-Site Request Forgery (CSRF) is a common type of cyber-attack that leaves any HTTP POST request vulnerable to spoofing. During a CSRF, a malicious website will request or send information to a vulnerable site where a user is currently logged in. The vulnerable site does not care that the request came from another website, and happily completes the request.
Here is an example of a CSRF attack:
- A user logs into a legitimate web site and receives an authentication cookie.
- The user does not log out and visits a malicious website. This site contains a form that posts to the legitimate website.
- The user fills out the form and submits it. Since the browser still has the authentication token,
- The legitimate website server runs the malicious request.
Since the user has been authenticated, the malicious site can do anything the authenticated user is allowed to do. Unfortunately, security measures such as SSL do nothing to protect against CSRF attacks because forged requests can easily be sent over HTTPS. Additionally, a CSRF attack does not actually require a user to fill out or submit any forms; the malicious website could easily use a script that runs when a user simply visits the website.
ASP.NET MVC uses Anti-Forgery Tokens to prevent CSRF. The idea is simple: an MVC controller expects an authentication token with a same-origin policy in the header of the HTTP Post request. The same-origin policy prevents malicious sites from reading a user’s tokens. Thus, the controller will only process the requests that contain a valid anti-forgery token. Any unsafe methods, such as POST, PUT, and DELETE, should be safeguarded in this way.
AJAX, Validation, and CSRF
Now the only question is: how can we make all this work together? In the following example, I will show you how to add an Anti-Forgery token to your controller methods called by AJAX.
1. Create MVC Project
For this example, I am using the following environment:
- Visual Studio 2017
- NET Core 2.1
- Bootstrap 3.4.1
- JQuery 3.3.1
- JQuery Validate 1.17.0
- JQuery Validation Unobtrusive 3.2.9
First, we will create a new ASP.NET Core Web Application. For this example, we do not require HTTPS or authentication.
We can run the project right away to see the default template and to make sure everything is working properly.
2. Create Form
We will create a simple form to receive messages from customers. We start by building the following view model. Notice the data attributes placed on each property. These attributes will work with the tag helpers on the front end and generate appropriate labels and validations messages within the form itself. By setting up the view model this way, validation will be essentially automatic through JQuery Validation.
I created this view model in CSRFExample/Models:
Now that we have created the view model, our form will be using; we can create the form itself. The ‘Contact’ page is the most logical place to put our form, so open up Views/Home/Index.cshtml, and we can add our markup.
The simple form should look something like this when done:
On this side, we have created a hidden input field to hold our Anti-Forgery token. We have also injected NET Core’s IAntiforgery service directly into the view. By using the service’s GetAndStoreTokens method, we can receive a request token and place it into the RequestVerificationToken hidden input field. Normally, this can be done with an HTML helper method:
Or by using a tag helper in the form tag:
<form method=”post” asp-action=”SubmitContactForm” as-antiforgery=”true”></from>
However, using AJAX complicates this a bit, and we will want to retrieve the token using jQuery when the form is submitted.
3. The Controller Method
Since we are using MVC, we will need to create a Controller Action to handle the form submission. This endpoint is what we will want to protect with an Anti-Forgery token. For simplicity’s sake, we will create the Action in the HomeController.
This example controller action doesn’t do much of anything except validate the anti-forgery token and validate the view model. In our case, as discussed in Part 1, validating the view model with ModelState.IsValid on the server-side is a safety measure in case front end validation is somehow bypassed. It also allows for a specific action to be taken, such as logging the error, returning a message to the front end, or sending an email. The “ValidateAntiForgeryToken” annotation tells the action to expect an anti-forgery token. If the token is not available or invalid, validation will fail, and the action method will not execute.
4. Front End Code
Now that we have everything in place, it’s time to wire everything together. After all, what good is a form that doesn’t do anything?
There’s a lot going on with this script so we will break it down step by step:
- This ASP.NET Core template includes the necessary front-end validation scripts to run validation. This includes jQuery Validation and jQuery Unobtrusive and is included in the page through a partial view.
- An event listener is placed on our form and will fire when the user submits. This will trigger a callback function which runs the script that handles the AJAX call to our controller action. We begin the function by showing a loading spinner, then we call “event.preventDefault()” to terminate normal submission processing.
- Since this callback function will run on form submission whether the form passes validation or not, we use jQuery Validate to check for a valid form. If the form is valid, we will proceed to the AJAX call. If not, we will terminate the function, since validation messages will display on their own.
- If the form passes validation, we will make our AJAX call to the “SubmitContactForm” controller action in the HomeController. Using jQuery, we can gather all the applicable fields and pass them to the AJAX request as JSON data. Likewise, we will use jQuery to retrieve our anti-forgery token and set it as the request header. The controller action will read the token from the request header and if it is valid, perform the action method. To verify the request header is working, try removing the header from the AJAX POST request and see if the controller action is called.
- Finally, we have several callback methods that allow for handling of server responses.
- The ‘success’ field defines a callback function that is called when the request completes successfully. This function can be used to indicate a successful POST to the user.
- The ‘error’ field defines a callback function that is called when the request completes with an error. This function can be used to indicate a server error occurred and provide appropriate feedback to the user.
- The ‘complete’ field defines a callback function that is called when the request finishes, whether successfully or not. This can be used to clean up the form or hide any loading visuals. In this example, we hide the loading spinner.
In today’s world of hackers, data breaches, and security flaws, it is essential to protect your web site from malicious intent. Cross-Site Request Forgery is just one way a site can become compromised and is a fairly common scenario. Users can protect themselves from CSRF by signing out of web applications when finished using them, or by clearing their browser cookies. However, as good stewards of the internet, it is up to us to do what we can to protect our users from vulnerabilities.
With the internet becoming more and more of a blend of server-side and front-end code, there is no need to sacrifice functionality for security. With a little bit of creativity and ingenuity, we can have the best of both worlds.