Preventing CSRF Attacks in an ASP.NET Core MVC

Introduction

Cross-site request Forgery (CSRF) is a type of attack where unauthorized commands are transmitted from a user that a web application trusts. This type of attack can force a user's browser to send unwanted requests to a web application on which the user is authenticated, potentially leading to unintended actions on the user's behalf.

How do CSRF Attacks work?

A typical CSRF attack involves the following steps,

  1. User Authentication: The user logs into a trusted website, which sets a session cookie in the browser.
  2. Malicious Site: The attacker tricks the user into visiting a malicious site.
  3. Crafted Request: The malicious site contains a crafted request that targets the trusted website.
  4. Request Execution: The browser, still authenticated with the trusted website, sends the forged request, which is then processed by the server as a legitimate request.

Real-World Example A Blog Site

Imagine a blog site where users can create posts by making a POST request to /Blog/Create with parameters like title, content, and csrf_token. Without proper CSRF protection, an attacker can create a form like this.

If you are not including the asp-antiforgery="true" then an attacker can submit the malicious data to your website.

@model XSSAttackInAspNetCoreMVC.Model.BlogPost

<!DOCTYPE html>
<html>
<head>
    <title>Create Blog Post</title>
</head>
<body>
    <h1>Create Blog Post</h1>
    <form asp-action="Create" method="post">
        <div class="form-group">
            <label asp-for="Title"></label>
            <input asp-for="Title" class="form-control" />
            <span asp-validation-for="Title" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="Content"></label>
            <textarea asp-for="Content" class="form-control"></textarea>
            <span asp-validation-for="Content" class="text-danger"></span>
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
    <a asp-controller="Blog" asp-action="Index">Back to List</a>
</body>
</html>

When the victim, while authenticated, visits the malicious site, this form could be submitted automatically, causing an unauthorized blog post to be created.

Mitigating CSRF in ASP.NET Core MVC

ASP.NET Core MVC provides built-in mechanisms to prevent CSRF attacks using anti-forgery tokens. Here's a step-by-step guide to implementing CSRF protection in an ASP.NET Core MVC blog site.

Step 1. Setting Up the ASP.NET Core MVC Application

First, create a new ASP.NET Core MVC application using the.NET CLI or Visual Studio.

dotnet new mvc -n BlogCSRFExample
cd CSRFAttackInAspNetCoreMVC

Step 2. Adding Anti-Forgery Tokens to Forms

In your MVC views, include an anti-forgery token in your forms using the @Html.AntiForgeryToken() helper. This adds a hidden field with a unique token for the session.

Example: Create Blog Post View (Create.cshtml)

@model XSSAttackInAspNetCoreMVC.Model.BlogPost

<!DOCTYPE html>
<html>
<head>
    <title>Create Blog Post</title>
</head>
<body>
    <h1>Create Blog Post</h1>
    <form asp-action="Create" method="post" asp-antiforgery="true">
        @Html.AntiForgeryToken()
        <div class="form-group">
            <label asp-for="Title"></label>
            <input asp-for="Title" class="form-control" />
            <span asp-validation-for="Title" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="Content"></label>
            <textarea asp-for="Content" class="form-control"></textarea>
            <span asp-validation-for="Content" class="text-danger"></span>
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
    <a asp-controller="Blog" asp-action="Index">Back to List</a>
</body>
</html>

Step 3. Validating the Anti-Forgery Token in the Controller

In your controller, ensure that the anti-forgery token is validated by decorating your actions with the [ValidateAntiForgeryToken] attribute.

Example: Blog Controller (BlogController.cs)

using CSRFAttackInAspNetCoreMVC.Data;
using CSRFAttackInAspNetCoreMVC.Model;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Security.Application;

namespace CSRFAttackInAspNetCoreMVC.Controllers
{
    public class BlogController : Controller
    {
        private readonly ApplicationDbContext _context;

        public BlogController(ApplicationDbContext context)
        {
            _context = context;
        }

        [HttpGet]
        public IActionResult Index()
        {
            var posts = _context.BlogPosts.ToList();
            return View(posts);
        }

        [HttpGet]
        public IActionResult Create()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Create(BlogPost model)
        {
            if (ModelState.IsValid)
            {
                // Sanitize inputs before saving to the database
                model.Title = Sanitizer.GetSafeHtmlFragment(model.Title);
                model.Content = Sanitizer.GetSafeHtmlFragment(model.Content);

                _context.BlogPosts.Add(model);
                _context.SaveChanges();
                return RedirectToAction(nameof(Index));
            }

            return View(model);
        }
    }
}
}

Step 4. Enforcing Global Anti-Forgery Token Validation

To reduce the risk of missing the attribute on individual actions, you can enforce global anti-forgery token validation by adding a filter in Startup.cs.

Example: Adding Global Anti-Forgery Token Validation (Startup.cs)

using CSRFAttackInAspNetCoreMVC.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Mvc;

namespace CSRFAttackInAspNetCoreMVC
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddRazorPages();
            builder.Services.AddControllersWithViews(options =>
            {
                options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
            });
            builder.Services.AddDbContext<ApplicationDbContext>(o =>
                o.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

            // Add Anti-Forgery token services
            builder.Services.AddAntiforgery(options =>
            {
                options.HeaderName = "X-CSRF-TOKEN"; // Customize as needed
            });

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication(); // Add this line if using authentication
            app.UseAuthorization();

            // Apply Anti-Forgery Token Middleware
            app.Use(next => context =>
            {
                if (
                    string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(context.Request.Method, "PUT", StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(context.Request.Method, "DELETE", StringComparison.OrdinalIgnoreCase))
                {
                    var antiforgery = context.RequestServices.GetRequiredService<IAntiforgery>();
                    antiforgery.ValidateRequestAsync(context).Wait();
                }
                return next(context);
            });

            app.MapRazorPages();
            app.MapControllers(); // Use this instead of UseEndpoints for mapping controllers

            app.Run();
        }
    }
}

Step 5. Configuring Anti-Forgery Token Options

Customize the anti-forgery token behavior by configuring options in Startup.cs.

Example: Customizing Anti-Forgery Token Options (Startup.cs)

public void ConfigureServices(IServiceCollection services)
{
    services.AddAntiforgery(options =>
    {
        options.Cookie.Name = "X-CSRF-TOKEN-COOKIE";
        options.HeaderName = "X-CSRF-TOKEN-HEADER";
    });

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

GitHub Project Url

https://github.com/SardarMudassarAliKhan/CSRFAttackInAspNetCoreMVC.git

Conclusion

CSRF attacks exploit the trust a website has in the user's browser to perform unwanted actions. ASP.NET Core MVC provides robust built-in tools to prevent CSRF attacks using anti-forgery tokens. By including these tokens in your forms and validating them in your controllers, you can significantly mitigate the risk of CSRF attacks. Implementing these practices ensures your blog site maintains high-security standards, protecting both your application and your users.