Post Cover

Understanding How Antiforgerytoken works in ASP.NET Core MVC

ASP.NET Core Posted May 09, 2021

Imagine there is a web application form which updates user's mobile number associated to a banking account. The form takes in the user's mobile number and his account number and submits the update to the backend system.

While a real-world implementation might be a lot complex than this, for brevity sake let's assume that the user's bank is so dumb that it just uses this form as the source for user's data update.

Now imagine if there was another impersonating user who submits the same form from another domain, with a dubious application with the user's account number with his own mobile number. That would be quite a disastrous if the application allows such a form submission.

Now all the updates or control over the original owner's account would go to this impersonating user aka hacker, all because the web backend allows such an update.

This vulnerability of forging requests from another domain instead of the trusted domain to the system is called as the Cross-Site Request Forgery (CSRF or XSRF), which is a security issue need to be handled by the web applications that contain user forms and interactions.

Now how do we ensure that form submissions happen from the same domain and no hacker can try submit data to the backend from other domains? In other words, how do we ensure our application is resistant to such CSRF attacks? Enter Antiforgerytoken - a technique available from the initial times of ASP.NET MVC, which is available in AspNetCore in a much more simpler way.

What is Antiforgerytoken?

Antiforgerytoken is a controller attribute which can be decorated over a controller action that is suceptible to CSRF attacks. To understand how CSRF happens and Antiforgerytoken works, let's look at the below example:

Let's create two AspNetCore MVC applications, which represent an original web application where user interactions happen, and a dubious application where user is tricked into forgery.

> mkdir csrfdemo
> dotnet new mvc --name normalwebapp
> dotnet new mvc --name csrfwebapp

Inside the normalwebapp project, let's modify the Home page to contain a simple form that takes in the Account Number and the Mobile Number and submits the same to the backend controller.

<!-- ~\normalwebapp\Views\Home\Index.cshtml -->

<form method="POST">
    <input type="text" name="mobilenumber" placeholder="Enter your Mobile Number" />
    <input type="text" name="accountnumber" placeholder="Enter your Account Number" />
    <button class="btn btn-primary" type="submit">Update</button>
</form>

data/Admin/2021/5/normalwebapp_screen.PNG

The values are POSTed to the backend controller action Index, which say returns a String Content saying that the update was successful.

[HttpPost]
public IActionResult Index(UpdateAccountModel model)
{
    return Content($"Account {model.AccountNumber} is updated with the new Mobile Number {model.MobileNumber}");
}

When this application is executed and the form is submitted, the result can be something like this:

// HTML document https://localhost:5001/Home/Index

Account 1234567890 is updated with the new Mobile Number +11234567890

Now let's modify the other application csrfwebapp with its Index page containing a single button visible to the user as below.

<div class="row">
    <div class="col-md-12 text-center">
        <!-- some random content -->
        <p>
            Man request adapted spirits set pressed. Up to denoting subjects sensible feelings it indulged directly. We
            dwelling elegance do shutters appetite yourself diverted. Our next drew much you with rank. Tore many held
            age hold rose than our. She literature sentiments any contrasted. Set aware joy sense young now tears china
            shy.
        </p>
        <form method="POST" action="https://localhost:5001/Home/Index">
            <input type="hidden" name="mobilenumber" value="9012345678" />
            <input type="hidden" name="accountnumber" value="1234567890" />
            <button class="btn btn-primary" type="submit">Click me to have some fun!</button>
        </form>
        <p>
            Ferrars all spirits his imagine effects amongst neither. It bachelor cheerful of mistaken. Tore has sons put
            upon wife use bred seen. Its dissimilar invitation ten has discretion unreserved. Had you him humoured
            jointure ask expenses learning. Blush on in jokes sense do do. Brother hundred he assured reached on up no.
            On am nearer missed lovers. To it mother extent temper figure better.
        </p>
        <!-- some random content -->
    </div>
</div>

Observe that the button, although bearing a random text is actually submitting a form with hidden values to the original web backend, with accountnumber and mobilenumber fields attached. What's more interesting is that this actually works.

data/Admin/2021/5/csrfwebapp_screen.png

When we click on this button, the below text appears. Keep in mind that we're running the csrfwebapp project on the domain localhost:8084, which is technically a different domain from the localhost:5001 where our original web backend runs. So the interaction that happens here is actually "Cross-Site".

// HTML document https://localhost:5001/Home/Index

Account 1234567890 is updated with the new Mobile Number 9012345678

Something which shouldn't have happened has been working; the backend is accepting values from a different form with seemingly ingenuine values. This we called is a Cross-Site Request Forgery.

In AspNetCore solving this quite simple. Just decorate the Index action with [ValidateAntiForgeryToken] attribute.

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(UpdateAccountModel model)
{
    return Content($"Account {model.AccountNumber} is updated with the new Mobile Number {model.MobileNumber}");
}

Now when we run this application, the form at https://localhost:5001/Home/Index works without any issues, while when we click on the button at http://localhost:8084 which is the other dubious application, this time no content is shown on the result. Instead when we inspect the request we see that the form submit has returned a 400 from the server. Why?

How Antiforgerytoken Works?

To understand how Antiforgerytoken works, let's look at the HTML document in the https://localhost:5001/Home/Index where the actual update form exists. It looks like below:

<form method="POST">
    <input type="text" name="mobilenumber" placeholder="Enter your Mobile Number">
    <input type="text" name="accountnumber" placeholder="Enter your Account Number">
    <button class="btn btn-primary" type="submit">Update</button>
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8DzlC42800pAjeV0jCEKXjxqooUUjzqdtwmUOWSz1wOF-ZAbK_DXjiSIPrb94REJiGrfa2hxKMeW2Rcf4zjmo9ywGlTxtQDj2_MQ8hITysQjm9oy_fpCfab2KWNdJ6efPa3So-LaGKhfMICpHtVscqM"></form>

We can see that there's a hidden field added to the form, which goes by the name "__RequestVerificationToken". This hidden field represents the authenticity of the form, generated because the backend action requires it. When this form is submitted, this token is also passed on to the backend behind the scenes and the ValidateAntiForgeryToken header validates this value for authenticity and allows the submission to happen.

In the other case, when we inspect the other form we find no such hidden field in the form (other than the hidden fields we placed ourselves) and so when the form gets submitted, the action method receives no such token and its automatically rejected. This way the submission "Cross-Site" is restricted by Antiforgerytoken attribute.

We can place this ValidateAntiForgeryToken attribute on an action or on the controller class itself, which induces the Antiforgerytoken implementation across all of its actions where data submission is possible.

Types of Antiforgerytoken attributes in MVC - Configuring as a Global Filter:

other than ValidateAntiForgeryToken attribute, we also have AutoValidateAntiforgeryToken which applies Antiforgerytoken concept to all methods except HttpGet. One advantage is that we can use this attribute to add this as a global filter.

An attribute that causes validation of antiforgery tokens for all unsafe HTTP methods. An antiforgery token is required for HTTP methods other than GET, HEAD, OPTIONS, and TRACE.

services.AddControllersWithViews(options => {
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});

This results in scenarios, where we might need to disable Antiforgerytoken requirements. In such cases we decorate those actions or controllers with IgnoreAntiforgeryToken attribute.

// forgery token validation
// happens for all actions
// in the controller which are marked
// unsafe (HttpPost, HttpPost, HttpPatch)
[AutoValidateAntiforgeryToken]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    // token is not applied
    // because of the ignore attribute
    // on the action
    [HttpPost]
    [IgnoreAntiforgeryToken]
    public IActionResult Index(UpdateAccountModel model)
    {
        return Content($"Account {model.AccountNumber} is updated with {model.MobileNumber}");
    }

    // forgery token is applied
    // due to class level attribute
    [HttpPut]
    public IActionResult Privacy(UpdateAccountModel model)
    {
        // something happens here
        return View(model);
    }
}
Author-Image

Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity.

You can now show your support. 😊

We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept