How to secure your ASP.NET Azure Web App

One of the ASP.NET Azure Web applications I’ve been working with is going to be security audited tomorrow and I was “only checking” that everything is OK in my app security-wise. I figured that I should write down a cheat sheet for the future to make sure I have all the configurations needed in place in other apps as well.

So I gathered a list of configurations you should add to your web app’s Web.config and other things to consider when implementing a secure web app. There are some Azure specific configurations in this article but mostly they apply to on-prem apps as well.

Remove unnecessary HTTP headers

Revealing system data or debugging information helps an attacker to learn about the system and form a plan of attack accordingly. Information leakage is not a direct attack, it just helps an attacker to gather information about the system which helps him to do a more serious attack.

Default Headers by ASP.NET MVC

Unnecessary HTTP headers should be removed from your responses. X-AspNet-VersionX-AspNetMvc-VersionX-Powered-By, and Server HTTP headers provide no direct benefit and unnecessarily use a small amount of bandwidth and expose information about your service.

<system.web>  
    <httpRuntime enableVersionHeader="false" />
</system.web>  
<system.webServer>  
    <httpProtocol>
      <customHeaders>
        <add name="X-Frame-Options" value="DENY" />
        <remove name="X-Powered-By" />
      </customHeaders>
    </httpProtocol>
    <security>
      <!-- removes Server HTTP header in Azure -->
      <requestFiltering removeServerHeader="true" />
    </security>
</system.webServer>  

ASP.NET MVC 5 uses the X-Frame-Options: SAMEORIGIN header automatically so you might want to remove that so you don’t end up with duplicate headers. You can do it by adding this line to your Global.asax.cs Application_Start() method:

// removes duplicate X-Frame-Options header
AntiForgeryConfig.SuppressXFrameOptionsHeader = true;  

Note: VS IntelliSense doesn’t understand removeServerHeader attribute on requestFiltering. Worry not, it works.

Force HTTPS and enable HSTS

<system.webServer>  
    <rewrite>
      <rules>
        <rule name="Force HTTPS" enabled="true">
          <match url="(.*)" ignoreCase="false" />
          <conditions>
            <add input="{HTTPS}" pattern="off" />
          </conditions>
          <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" appendQueryString="true" redirectType="Permanent" />
        </rule>
      </rules>
      <outboundRules>
        <rule name="Add Strict-Transport-Security when HTTPS" enabled="true">
          <match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
          <conditions>
            <add input="{HTTPS}" pattern="on" ignoreCase="true" />
          </conditions>
          <action type="Rewrite" value="max-age=31536000" />
        </rule>
      </outboundRules>
    </rewrite>
</system.webServer>  

This piece of configuration I picked up from The Great Hanselman’s blog: http://www.hanselman.com/blog/HowToEnableHTTPStrictTransportSecurityHSTSInIIS7.aspx

Secure cookies

Make your cookies secure by making them httpOnly and requireSSL.

<system.web>  
    <httpCookies httpOnlyCookies="true" requireSSL="true" />    
</system.web>  

https://www.owasp.org/index.php/HttpOnly

Rename ASP.NET cookies

Like X-AspNet-VersionX-AspNetMvc-VersionX-Powered-By, and Server HTTP headers, the default ASP.NET Cookies, AspNet.ApplicationCookieASP.NET_SessionId and __RequestVerificationToken, exposes the fact that the site is running on ASP.NET.

Default named cookies on asp.net application

It is wise to rename those cookies. Here is how you do it with ASP.NET MVC:

Modify Startup.Auth.cs accordingly:

app.UseCookieAuthentication(new CookieAuthenticationOptions  
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
    Provider = new CookieAuthenticationProvider
    {
        OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<UserManager, ApplicationUser>(
            validateInterval: TimeSpan.FromMinutes(0),
            regenerateIdentity: (manager, user) => manager.GenerateUserIdentityAsync(user))
    },
    CookieName = "appcookie",
});

Notice the CookieName parameter on CookieAuthenticationOptions. Now the application cookie will be named as appcookie when the user logs in.

Global.asax.cs

You can rename CSRF token cookie by adding the following line to Global.asax.cs:

AntiForgeryConfig.CookieName = "xsrftoken";  

Rename session cookie in Web.config

<system.web>  
    <sessionState timeout="30" cookieName="sessionid" />
</system.web>  

Now you should have custom named cookies that won’t expose your site being ASP.NET.

ASP.NET cookies renamed

Use custom errors and disable tracing

Don’t forget to disable tracing (trace.axd) and error stack traces in Azure. At worst, these might expose critical information about your application to the attacker such as connection strings or other credentials, coding style, physical location of your application or virtual directory path, asp.net version etc.

<system.web>  
    <customErrors mode="RemoteOnly" defaultRedirect="~/Error.aspx" />
    <trace enabled="false" />
</system.web>  

Note: Always publish on Release mode. This will also turn on custom errors and disable tracing.

Disable Caching for pages that contains sensitive data

Use proper HTTP headers to disable caching of sensitive data.

Cache-Control: no-cache, no-store, must-revalidate  
Pragma: no-cache  
Expires: 0  

You can set these headers in ASP.NET MVC like this:

Response.Cache.SetCacheability(HttpCacheability.NoCache);  // HTTP 1.1.  
Response.Cache.AppendCacheExtension("no-store, must-revalidate");  
Response.AppendHeader("Pragma", "no-cache"); // HTTP 1.0.  
Response.AppendHeader("Expires", "0"); // Proxies.  

Turn off auto-complete on forms or specific inputs that are collecting sensitive data:

<form autocomplete="off">  
<input autocomplete="off" />  

Read more at http://stackoverflow.com/questions/49547/making-sure-a-web-page-is-not-cached-across-all-browsers

Add Content-Security-Policy (CSP) header to prevent XSS

Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross Site Scripting (XSS) and data injection attacks. These attacks are used for everything from data theft to site defacement or distribution of malware.

Source: developer.mozilla.org

I am not going to get too deep into CSP but I encourage to check out what developer.mozilla.org has to tell about it.

I am going to dig deeper into this subject in another post so to keep this one short, here is what my CSP header looks like:

<system.webServer>  
  <httpProtocol>
    <customHeaders>
      <add name="Content-Security-Policy" value="
        default-src 'self';
        style-src 'self' 'unsafe-inline';
        script-src 'self' https://*.vo.msecnd.net;
        img-src 'self' *.blob.core.windows.net;
        connect-src wss: https:;
      " />
    </customHeaders>
  </httpProtocol>
</system.webServer>  

I’m allowing everything from 'self' by default and disable everything else. I’ve done some exceptions for allowing Application Insights analytics, images from my Azure Blob Storage and inline CSS. I had some inline JavaScript for getting the user id from server-side to a global JS variable that I had to get rid of so that I wouldn’t have to make any exceptions by hash or nonce tricks. In short: I created a .cshtml file that had my former inline JS in it and included it in <head> with a <script> tag. As said, there’s going to be a ‘deep-dive’ kind of post on this subject. Stay tuned.

Add X-Xss-Protection and X-Content-Type-Options headers

X-Xss-Protection header is used to configure the built in reflective XSS protection found in Internet Explorer, Chrome and Safari (Webkit). Valid settings for the header are 0, which disables the protection, 1 which enables the protection and 1; mode=block which tells the browser to block the response if it detects an attack rather than sanitising the script.

X-Content-Type-Options header is nice and easy to configure, this header only has one valid value, nosniff. It prevents Google Chrome and Internet Explorer from trying to mime-sniff the content-type of a response away from the one being declared by the server. It reduces exposure to drive-by downloads and the risks of user uploaded content that, with clever naming, could be treated as a different content-type, like an executable.

Quotes by Scott Helme, the author of securityheaders.io.

<system.webServer>  
  <httpProtocol>
    <customHeaders>
      <add name="X-Xss-Protection" value="1; mode=block" />
      <add name="X-Content-Type-Options" value="nosniff" />
    </customHeaders>
  </httpProtocol>
</system.webServer>  

Read more here: https://scotthelme.co.uk/hardening-your-http-response-headers/#x-xss-protection

Session fixation and server-side auth token invalidation

Nick Coblentz has written a good summary of session fixation and server-side auth token invalidation vulnerabilities:

ASP.NET_SessionId Alone: Session Fixation

“There are three common ways to use these cookies that result in risk. First, when the ASP.NETSessionId cookie is used alone, the application is vulnerable to session fixation attacks. The root cause of this vulnerability is that the ASP.NETSessionId cookie value isn’t changed or regenerated after users log in (or cross any kind of authentication boundary). In fact, Session IDs are intentionally reused in ASP.NET. If an attacker steals an ASP.NET_SessionId prior to a victim authenticating, then the attacker can use the cookie value to impersonate the victim after he or she logs in. This gives the attacker unauthorized access to the victim’s account.”

Forms Authentication Cookie Alone: Can’t Terminate Authentication Token on the Server

“Second, when a forms authentication cookie is used alone, applications give users (and potentially attackers) control over when to end a session. This occurs because the forms authentication ticket is an encrypted set of fields stored only on the client-side. The server can only request that users stop using the value when they log out. The ASP.NET framework does not have a built-in feature to invalidate the cookie on the server-side. That means, clients (or attackers) can continue using a forms authentication ticket even after logged out. This allows an attacker to continue using a stolen forms authentication token despite a user logging out to protect him or herself.”

How to see it in action?

You can test these vulnerabilities by following these steps:

  • Log in to your application
  • Copy the value of your application cookie
  • Log out
  • Open up a different browser and do not log in
  • Use e.g. EditThisCookie Chrome extension and paste your application cookie
  • Refresh the page and get your mind blown.

The next time you will leave your app open and go AFK without locking up your desktop, look over your shoulder. It takes 3 seconds to copy your auth cookie and the attacker can start using the app peacefully impersonating you on his/her own computer without you ever noticing it.

Solution in ASP.NET MVC 5

I enhanced Nick’s solution slightly by adding “proper” session invalidation. Make the following adjustments to your application:

ValidateAuthenticationFilter.cs (Action Filter)

public class ValidateAuthenticationFilter : ActionFilterAttribute  
{
    /// <summary>
    /// Validates LoggedInSessionID set in Login action. If SessionID doesn't match, 
    /// session will be invalidated and user will be redirected to login page.
    /// </summary>
    /// <param name="filterContext"></param>
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext.HttpContext.Session != null)
        {
            // Get LoggedInSessionID property from the session
            var loggedInSessionId = filterContext.HttpContext.Session["LoggedInSessionID"] as string;

            if (filterContext.HttpContext.Request.IsAuthenticated)
            {
                // Do the actual SessionID and LoggedInSessionID match?
                if (loggedInSessionId == null || loggedInSessionId != filterContext.HttpContext.Session.SessionID)
                {
                    InvalidateAuthAndRedirectToLogin(filterContext);
                }
            }
            else if (loggedInSessionId != null)
            {
                InvalidateAuthAndRedirectToLogin(filterContext);
            }
        }
        else
        {
            InvalidateAuthAndRedirectToLogin(filterContext);
        }
        base.OnActionExecuting(filterContext);
    }

    private static IAuthenticationManager AuthenticationManager => HttpContext.Current.GetOwinContext().Authentication;

    /// <summary>
    /// Regenerate Session ID and clear session data
    /// </summary>
    public static void InvalidateSession()
    {
        var manager = new SessionIDManager();
        var newSessionId = manager.CreateSessionID(HttpContext.Current);
        bool redirected, isAdded;
        manager.SaveSessionID(HttpContext.Current, newSessionId, out redirected, out isAdded);
        HttpContext.Current.Session.Clear();
        HttpContext.Current.Session.RemoveAll();
        HttpContext.Current.Session.Abandon();
    }

    private static void InvalidateAuthAndRedirectToLogin(ActionExecutingContext filterContext)
    {
        AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
        InvalidateSession();
        //redirect to the login page
        filterContext.Result =
            new RedirectToRouteResult(new RouteValueDictionary
            {
                {"action", "Index"},
                {"controller", "Home"}
            });
    }
}

Add ValidateAuthenticationFilter to GlobalFilters in Global.asax.cs

protected void Application_Start()  
{
    // Register ValidateAuthenticationFilter for all controllers
    GlobalFilters.Filters.Add(new ValidateAuthenticationFilter());
}

Add session property when user logs in (AccountController.cs)

// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)  
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
    switch (result)
    {
        case SignInStatus.Success:
            ValidateAuthenticationFilter.InvalidateSession(); <!-- START A NEW SESSION -->
            Session["LoggedInSessionID"] = Session.SessionID; <!-- THIS IS USED IN VALIDATEAUTHENTICATIONFILTER -->
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            ...
        case SignInStatus.RequiresVerification:
            ...
        case SignInStatus.Failure:
        default:
            ...
    }
}

Invalidate session on log out

// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()  
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
    ValidateAuthenticationFilter.InvalidateSession();
    return Redirect("/#!/");
}

And that’s a wrap. Now the session and auth cookie are coupled and session is properly invalidated.

Note: If the attacker is able to copy your sessionid and appcookie cookie values, he/she will still be able to log in as you. This solution only helps you if you logout from the app.

Test your website configuration vulnerabilites easily with ASafaWeb and securityheaders.io

ASafaWeb is the Automated Security Analyser for ASP.NET Websites and it’s a free on-demand service located at asafaweb.com. The goal of ASafaWeb is to help ASP.NET website developers quickly identify security misconfiguration risks within their sites. The service simply takes a publicly facing URL then makes a number of non-malicious requests to the site to establish the security profile and provide guidance on how it can be further strengthened.

ASAFAWeb

Read more about ASafaWeb here:

securityheaders.io is an easy way of checking that you have all the security headers in place. I got an A with configurations presented in this article.

Other security measures

Of course there are other things to consider when implementing a secure web app like SQL injections, CSRF (Anti-Forgery Tokens ftw), XSS etc. I strongly recommend checking out the OWASP Top 10 vulnerabilities and consider if your application is exposing any of these vulnerabilities.

Check out the OWASP .NET Security Cheat Sheet too!

Edit. Security audit is done now. Turned out that I was still missing Content-Security-Policy header and haven’t fixed session fixation vulnerability. I edited the article to include solutions to those as well.