ASP.NET Core Basic Security Settings Cheatsheet

When starting a new project, looking at a new framework, or fixing vulnerabilities identified as part of an assessment or tool, its nice to have one place to refer to the fixes for common security issues. This post provides solutions for some of the more basic issues, especially those around configuration. Most of these answers can be found in Microsoft's documentation or by doing a quick Google search. But hopefully having it all right here will save others some time.

Enabling An Account Lockout Response

To enable the account lockout response for ASP.NET Identity, first modify the Startup.cs file to choose appropriate settings. In the ConfigureServices method, add the following code:
services.Configure<IdentityOptions>(options =>
{
  //optional
  options.Lockout.AllowedForNewUsers = true;
  //requires manual unlock
  options.Lockout.DefaultLockoutTimeSpan = TimeSpan.MaxValue;
  //three failed attempts before lockout
  options.Lockout.MaxFailedAccessAttempts = 3; 
});
With the settings configured, lockout still needs to be enabled in the login method of the account controller. In AccountController -> Login(LoginViewModel model, string returnUrl = null), change lockoutOnFailure from false to true as shown below:
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);

References:
ASP.NET Identity comes with a class that validates passwords. It is configurable and allows one to decide if passwords should require a digit, uppercase letters, lowercase letters, numbers, and/or a symbol. This policy can be further customized by implementing the IPasswordValidator interface or extending the Microsoft.AspNetCore.Identity.PasswordValidator. The code below extends the PasswordValidator and ensures the password does not contain an individual's username.
using ASPNETCoreKestrelResearch.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ASPNETCoreKestrelResearch.Security
{
  public class CustomPasswordValidator<TUser> : PasswordValidator<TUser> where TUser : IdentityUser
    {
        public override async Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user, string password)
        {            
            IdentityResult baseResult = await base.ValidateAsync(manager, user, password);

            if (!baseResult.Succeeded)
                return baseResult;
            else
            {
                if (password.ToLower().Contains(user.UserName.ToLower()))
                {                    
                    return IdentityResult.Failed(new IdentityError
                    {
                        Code = "UsernameInPassword",
                        Description = "Your password cannot contain your username"
                    });
                }
                else
                    return IdentityResult.Success;
            }
        }
    }
}
Next, ASP.NET Identity needs to be told to use that class. In the ConfigureServices method of Startup.cs, find services.AddIdentity and add ".AddPasswordValidator<CustomPasswordValidator<ApplicationUser>>();" as shown below.
services.AddIdentity<ApplicationUser, IdentityRole>()
  .AddEntityFrameworkStores<ApplicationDbContext>()
  .AddDefaultTokenProviders()
  .AddPasswordValidator<CustomPasswordValidator<ApplicationUser>>();

Choosing a Session Timeout Value

Developers can choose how long a session cookie remains valid and whether a sliding expiration should be used by adding the following code to the ConfigureServices method of Startup.cs:
services.Configure<IdentityOptions>(options =>
{
  options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromMinutes(10);
  options.Cookies.ApplicationCookie.SlidingExpiration = true;
});

Enabling the HTTPOnly and Secure Flag for Authentication Cookies

First, if you are using Kestrel, HTTPS (TLS) is not supported. Instead, it is implemented by HAProxy, Nginix, Apache, IIS, or some other web server you place in front of the application. If you are using Kestrel, the Secure flag cannot be enabled properly from the application code. However, if you are hosting the application in IIS directly, then it will work. The following code demonstrates enabling both the HTTPOnly and Secure flags for cookie middleware in ASP.NET Identity through the ConfigureServices method in Startup.cs.
services.Configure<IdentityOptions>(options =>
{
  options.Cookies.ApplicationCookie.CookieHttpOnly = true;
  options.Cookies.ApplicationCookie.CookieSecure = CookieSecurePolicy.Always;
});

Enabling Cache-Control: no-store

When applications contain sensitive information that should not be stored on a user's local hard drive, The Cache-Control: no-store HTTP response header can help provide that guidance to browsers. To enable that feature, add the following code to the ConfigureServices method in Startup.cs.
services.Configure<MvcOptions>(options =>
{
  options.CacheProfiles.Add("DefaultNoCacheProfile", new CacheProfile
  {
    NoStore = true,
    Location = ResponseCacheLocation.None
  });
  options.Filters.Add(new ResponseCacheAttribute
  {
    CacheProfileName = "DefaultNoCacheProfile"                    
  });
});

Disabling the Browser's Autocomplete Feature for Login Forms

The changes to ASP.NET's razor views makes this super simple. Just add the autocomplere="off" attribute as if it were a normal HTML input field:
<input asp-for="Email" class="form-control" autocomplete="off"/>
<input asp-for="Password" class="form-control" autocomplete="off"/>

Modify The Iterations Count for the Password Hasher's Key Derivation Function

First, I believe the default right now is 10,000 and the algorithm is PBKDF2. The code below won't change that default iteration count, but it will show how it can be done. In ConfigureService in Startup.cs add the following code.
services.Configure<PasswordHasherOptions>(options =>
{                
  options.IterationCount = 10000;
});

Enforcing HTTPS and Choosing Appropriate TLS Protocols and Cipher Suites

As mentioned above, if you are using Kestrel you won't be able to use HTTPS directly. Therefore, you won't do this in your code. You will need to look up how to do this in HAProxy, Nginx, Apache, IIS, etc. If you are hosting your application using IIS directly, then you can enforce the use of HTTPS using something like https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Core/RequireHttpsAttribute.cs BUT, it will only be applied to your MVC controllers/views. It will not be enforced for static content (see https://github.com/aspnet/Home/issues/895). If you want to do this in code, you will need to write some middleware to enforce it across the entire application. Finally, the choice of cipher suites offered cannot be changed using code.

Enabling a Global Error Handler

A custom global error handler is demonstrated by the Visual Studio template. The following relevant code can be found in the Configure method of Startup.cs.
if (env.IsDevelopment())
{
  app.UseDeveloperExceptionPage();
  app.UseDatabaseErrorPage();
  app.UseBrowserLink();
}
else
{
  app.UseExceptionHandler("/Home/Error");
}

Removing the Server HTTP Response Header

All responses from the server are going to return "Server: Kestrel" by default. To remove that value, modify UseKestrel() in Program.cs to include the following settings change:
public static void Main(string[] args)
{
  var host = new WebHostBuilder()
    .UseKestrel(options =>
    {
      options.AddServerHeader = false;
    })
  .UseContentRoot(Directory.GetCurrentDirectory())
  .UseIISIntegration()
  .UseStartup<Startup>()
  .UseUrls("http://0.0.0.0:5000")
  .Build();

  host.Run();
}

X-Frame-Options, Content-Security-Policy, and Strict-Transport-Security HTTP Response Headers

The following post seems to cover most of these headers well: http://andrewlock.net/adding-default-security-headers-in-asp-net-core/. I haven't evaluated its design, but I did verify that I can install it and the headers are added successfully. Since Kestrel does not support HTTPS, consider whether its appropriate to implement the Strict-Transport-Security header using code or by configuring the web server placed in front of the application.

I installed this nuget package using "Install-Package NetEscapades.AspNetCore.SecurityHeaders". Then, I made sure to have the following imports in Startup.cs:
using NetEscapades.AspNetCore.SecurityHeaders;
using NetEscapades.AspNetCore.SecurityHeaders.Infrastructure;
I added the following code to the ConfigureService method of Startup.cs:
services.AddCustomHeaders();
Last, I added this code to the Configure method of Startup.cs:
app.UseCustomHeadersMiddleware(new HeaderPolicyCollection()
  .AddContentTypeOptionsNoSniff()
  .AddFrameOptionsDeny()
  //.AddStrictTransportSecurityMaxAge()
  .AddXssProtectionBlock()
  //.AddCustomHeader("Content-Security-Policy", "somevaluehere")
  //.AddCustomHeader("X-Content-Security-Policy", "somevaluehere")
  //.AddCustomHeader("X-Webkit-CSP", "somevaluehere")
);
Make sure you add this code BEFORE app.UseStaticFiles();, otherwise the headers will not be applied to your static files.

0 comments:

Post a Comment