ASP.NET Core Top Security Tips - Windows ASP.NET Core Hosting 2024 | Review and ComparisonWindows ASP.NET Core Hosting 2024 | Review and Comparison

Microsoft’s ASP.NET Core enables users to more easily configure and secure their applications, building on the lessons learned from the original ASP.NET. The framework encourages best practices to prevent SQL injection flaws and cross-site scripting (XSS) in Razor views by default, provides a robust authentication and authorization solution, a Data Protection API that offers simplicity of configuration, and sensible defaults for session management.

What could possibly go wrong? Let’s break down a few scenarios where misusing security features and improperly overriding defaults may lead to serious vulnerabilities in your applications. We’ll focus on MVC-based ASP.NET Core applications; however, most of the scenarios are equally applicable to Razor Pages.

Not validating anti-forgery tokens properly

Cross-Site Request Forgery (CSRF) attacks allow an attacker to trick a user into performing an action on a trusted web application, typically through getting the user to click on a link created by the attacker that will call the vulnerable application. A vulnerable application would have no idea that the malicious request triggered by the user was not intentional, and it would perform it. If the user was logged in during this time, the web browser would likely send the cookies with the request.

To protect against this, tokens should be created by the web application that are then passed back on each request to the server. These tokens change regularly, so a link provided by an attacker would be detected due to the outdated or missing token, and subsequently discarded by the application. Because CSRF relies on a stateful, pre-existing session and that the session information will be automatically passed via cookies, it is less likely to be required for API endpoints which are typically stateless.

ASP.NET Core provides a powerful toolset to prevent attacks using anti-forgery tokens. POST, PUT, PATCH and DELETE HTTP methods are the most likely to have significant side effects if REST guidelines have been followed, because these verbs are reserved for actions that alter state or data, and therefore they will require and validate anti-forgery tokens. For the sake of brevity we’ll use POST as an example from here on.

There are multiple ways to apply attribute-based filters to configure anti-forgery token validation, and the approaches may seem overwhelming:

  1. ValidateAntiForgeryToken applied to each POST action in the controllers that would be exposed to requests.
  2. ValidateAntiForgeryToken applied at the Controller level, exempting specific methods (most prominently those with GET actions) from validation using IgnoreAntiforgeryToken.
  3. AutoValidateAntiforgeryToken applied globally to validate tokens on all relevant requests by default, and using IgnoreAntiforgeryToken to opt out of validation for specific actions if necessary.

ASP.NET Core project templates and the code generation command-line interface creates controller actions that use approach (1) using the ValidateAntiForgeryToken attribute attached to every action associated with updating data – that is, ValidateAntiForgeryToken and HttpPost attributes are always used together:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> CreateSomething(Something something)

While the result of the approach is valid, if the developer is writing the methods manually, they may easily forget to include the ValidateAntiForgeryToken attribute alongside the attribute designating the action such as [HttpPost]. By default, neither ASP.NET Core nor the code editor will let you know about this, and you’ll likely not realize the mistake.

However, the repercussions can be very serious: POST requests to a particular action can now be initiated outside of your application, which opens a way for malicious users to execute CSRF. Option (2), with the Controller-level ValidateAntiForgeryTokenhas the same issue. If the developer forgets, the exposed actions for the entire Controller are susceptible to CSRF attacks.

There are two ways to prevent this:

  • Use a NuGet analyzer such as Security Code Scan that checks that all controller actions with HttpPost also have the ValidateAntiForgeryToken attribute and displays editor warnings otherwise. If you have a static security analysis tool integrated into your DevOps pipeline, it will serve as the second line of defense in case you’ve missed the editor warning.
  • Use Option (3): global automatic anti-forgery protection using  AutoValidateAntiforgeryToken. This is the solution that Microsoft recommends to ensure that all actions associated with updating data are protected by default while GET requests and other safer requests are automatically excluded from CSRF protection.

To enable global automatic anti-forgery protection for an MVC application based on ASP.NET Core 3.x, add a new filter to services.AddControllersWithViews() or services.AddMvc() under ConfigureServices() in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddControllersWithViews(
        options => options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));
    // ...
}

As soon as this is done, you can safely remove all individual ValidateAntiForgeryToken attributes from your controller POST actions.

Allowing SQL injections when using raw queries with EF Core

ASP.NET Core uses Entity Framework (EF) Core as its default Object Relational Mapping (ORM) implementation. As with any ORM, EF Core does a good job of preventing SQL injection attacks in most scenarios that would allow an attacker to subvert the intended operation of a SQL query. Developers should then use Language-Integrated Query (LINQ), which then generates parameterized SQL queries behind the scenes. These offer protection against SQL Injection that would require extra effort if string concatenation or interpolation was used.

EF Core still also allows executing raw SQL queries against the database however. This can be useful if LINQ generates an inefficient or unworkable query, or non-parameterisable data such as dynamic schema or table names need adding to the query. To execute raw SQL, EF Core provides methods such as DbSet.FromSqlRaw() and DbSet.FromSqlInterpolated() (or DbSet.FromSql() in EF Core prior to version 3.0).

If you need to call these methods, be careful when constructing SQL queries with user-supplied input. It’s easy to get confused and end up with code that allows SQL injection if that untrusted input has not been correctly supplied to these methods or if the input has not been validated.

For example, the following controller action is prone to SQL injection because it uses string interpolation to incorporate user input, whereas FromSqlRaw() expects user input to be passed separately from the SQL query template:

public async Task<IActionResult> FilteredPlayers(string filter)
{
    return View(await _context.Players.FromSqlRaw(
        $"select id, name from main.Players where FirstName like {filter}").ToListAsync());
}

There are several approaches to addressing this:

  • Is a raw query really the only way forward? It’s often worth checking what is available in Entity Framework’s LINQ functionality. It is regularly updated and an update may provide the features you need in order to avoid a raw SQL query. Raw queries should be considered for exceptions and not the norm so it would not be expected to find too many of these within a code base.
  • FromSqlRaw() allows additional parameters in the call:
return View(await _context.Players.FromSqlRaw("select id, name from main.Players where FirstName like {0}", filter).ToListAsync());

If you need more value placeholders, use {1}, {2} and so on, and add more values to the parameters passed to FromSqlRaw.

  • If you prefer string interpolation, use FromSqlInterpolated() instead of FromSqlRaw():
return View(await _context.Players.FromSqlInterpolated(
    $"select * from main.Players where FirstName like {filter}").ToListAsync());

It looks like our original flawed call, but the {filter} now becomes a placeholder for a parameter and FromSqlInterpolated creates the parameters for you.

  • You can use explicit SQL Parameters:
var firstName = new SqlParameter("firstname", filter);
 
return View(await _context.Players.FromSqlRaw($"select id, name from main.Players where FirstName like @firstname", firstName).ToListAsync());
  • If you have dynamic untrusted data that cannot be parameterized, it must instead be properly validated! This is often seen where tables or fields must be flexible due to the application design.
    • Check against an allow-list: if you only allow certain tables or fields to be included in the query, perform validation of the dynamic input against that list.
    • If you require more flexibility, if the database structure is fluid or user-configurable for example, consider checking against the allowed table or field format that is specific to the design of your application.
    • Check if it is a valid table against the database metadata.

Relying on hiding privileged UI as the only authorization check

When working on resources that should only be available to certain users, it’s easy to forget that access to such resources should be restricted both in views and in controllers. When this happens, you might end up hiding links to restricted resources from views, but not restricting the corresponding controller actions:

// This view doesn't show a link to a restricted resource to anonymous users
    @if (User.Identity.IsAuthenticated)
    {
        <p> <a asp-area="" asp-controller="Admin" asp-action="UserList">This is a hidden page!</a></p>
    }
    // However, the controller allows access to the restricted resource to anyone
    public IActionResult UserList() => View();

As a result, the application becomes exposed to forced browsing attacks: a malicious user could enter the direct URL to access the hidden page.

To prevent this, make sure to restrict access to controllers and/or controller actions through the [Authorize] attribute (and correctly configure the user roles):

[Authorize]
  public IActionResult UserList() => View();

Using developer exception pages and database error pages in production

This is not a direct security risk, but rather a serious information leak that can help an attacker to gain insights about your application’s internals and configuration that, in turn, could be useful when planning an attack.

When exposed to the malicious users, ASP.NET Core’s developer exception page (also known in earlier ASP.NET versions as the Yellow Screen of Death) can reveal a lot of debugging information about the running application, including middleware used, route patterns, as well as snippets of source code for views, controllers, or pages.

Similarly, the database error page leaks details such as the underlying database engine and table naming conventions.

Make sure to only use developer exception and database error pages while working in the development environment, as suggested by the default ASP.NET Core templates:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
    }

Summary

Out of the box ASP.NET Core provides several security protections configured with good default settings. However, as your application grows and becomes more complex, it’s easy to lose sight of important details, and details are what make or break application security.

To prevent vulnerabilities from hitting your business the hard way, make sure to review your own and each other’s code with security in mind, and incorporate static security analysis into your Software Development Lifecycle (SDLC) so that dangerous changes don’t go unnoticed.