12 strategies to quickly harden your .NET webapplication

Securing a website built on Microsoft’s MVC framework is not exceptionally hard. Out of the box, MVC offers a lot of protection against common OWASP attacks from the box. If you combine MVC with Entity Framework - like most of us - you don’t have to worry about SQL injection attacks either. But getting your MVC app through a penetration test is a bigger challenge. This post summarizes what I’ve learned so far, and applies to both MVC and WebForms (although some recommendations are less or not applicable).

What hackers are usually after: your sessionID

FormsAuthentication, SimpleMembership and most other authentication mechanisms are based on sessions. Whenever a user logs in they receive a unique session from the server which allows the user to re-enter the site without specifying the password again. Although the authentication session itself lives on the server, the visiting user somehow needs to be linked to this session. Most systems place a cookie with a sessionID (usually a long string) on the user’s computer to achieve this. 

A hacker who gains access to this cookie or can somehow figure out the sessionID has essentially found the key to take over the user’s session and log in as that user. Many attack strategies that hackers employ are aimed at gaining access to authentication cookies:

  • When the connection is not secured by SSL, the hacker can simply sniff network traffic to intercept authentication cookies;
  • Executing javascript or other malware in the user’s browser to extract the local cookie and send it somewhere;
  • When the connection is secured with SSL, the hacker break the encryption (if it’s weak);

Below are a number of changes you can make to your (MVC or WebForms) application to mitigate these risks.

1. Use Anti Forgery Tokens in all your forms

Hackers often try to post custom data from a remote server to your formhandlers, in hopes of getting it executed. Protect your handlers against Cross-Site Request Forgery (CSRF) with the AntiForgeryToken helper that is built into MVC:

<div class="form">
    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken();
     }
</div>

And don’t forget to add the [ValidateAntiForgeryToken] attribute on the action that handles the POST so that MVC actually verifies the token. This prevents hackers from posting to your handlers from remote servers, but it also prevents posting the form again from another machine or a second time.

2. Escape all user content when you write it to Html. Avoid @Html.Raw

Make sure to escape (or encode) all user-managed content that is shown somewhere on the website, like a username or a comment. Otherwise a hacker can inject javascript into your website, execute this within the context of another user’s browser and gain control over that user’s session.

MVC’s Razor Engine (MVC 3+) escapes Javascript in all content automatically. So you’re safe whenever you write content to your HTML with @[Model].[FieldName] or @Html.[FormHelper]. You can override this behavior with the helper @Html.Raw(string), but use this only for content that can’t be exploited by hackers.

3. Disable Debug mode and turn on friendly errors

Hackers use detailed error pages to see what’s happening under the hood. The more details your errors give away, the more dangerous it becomes. The best practice here is to disable Debug mode (which hides stack traces from errorpages) and configure a custom error page that shows a friendly error, but hides detailed information. 

Configuring this is easy. Just add or edit the following to your web.config:

<system.web>
  <customErrors mode="RemoteOnly" defaultRedirect="~/Error">
  <compilation debug="false"/>
</system.web>

You can also set the mode for customErrors to ‘On’. But I find it helpful to at least see a detailed error page on the webserver itself or on my local machine, which ‘RemoteOnly’ allows.

4. Protect cookies against snooping and prevent access by client-side scripts

Cookies are used to store user state. This often takes the form of a sessionID that uniquely identifies a user to the webserver. If hackers can somehow ‘read’ this sessionID from the cookie, they can create a custom cookie, paste in the sessionID and log in as another user. This is a very dangerous attack vector as it compromises your entire system.

Protecting your cookies requires two steps. The first is to make it very difficult for hackers to intercept cookies and read them. SSL is an excellent way to do this, as it encrypts the data (including cookies) that is passed between the user and the server. But cookies should never be sent to the server over a regular HTTP-connection.

The second step is to prevent cookies from being read by client-side scripts. Because even when you use SSL, a hacker can still access cookies if he or she can somehow gain access to the user’s browser window. They could, for example, inject javascript into the website that reads the cookies and transmit the contents to another server. The simplest way to protect against this is to mark cookies with the HTTP-ONLY flag. This blocks client-side scripts from accessing the cookie.

Configuring this is easy. Simply add or edit the following to your web.config:

<system.web>
    <httpCookies requireSSL="true" httpOnlyCookies=&rdquo;true&rdquo; />
    <authentication &hellip;&hellip;>
      <forms requireSSL="true" />
    </authentication>
</system.web>

5. Limit the chattiness of your application

By default, IIS and MVC add a few additional response headers that identify the version of IIS, MVC and ASP.NET. These headers don’t serve any purpose and are mostly used for usage statistics. Removing these headers is a good idea as they give hackers an edge. This includes X-POWERED-BY, SERVER, X-ASPNET-VERSION and X-ASPNETMVC-SESSION.

Removing the X-POWERED-BY header can be done in IIS (7+) by removing it in the ‘HTTP Response Headers’ module in IIS. Removing the other headers takes a bit more work and can be done in a number of ways. One approach is to remove the headers in the global.asax of your website:

protected void Application_PreSendRequestHeaders()
{
   Response.Headers.Remove("Server");
   Response.Headers.Remove("X-AspNet-Version");
   Response.Headers.Remove("X-AspNetMvc-Version");
}

Although this works, you do have to configure it for every individual application. Another approach is to use the URL Rewrite module for IIS to rewrite these (outgoing) headers with empty strings. More information can be found here.

6. Enable a strong expiration policy for authentication cookies

FormsAuthentication and SimpleMembership default to a 30-minute sliding expiration for sessions. Users that are inactive for 30 minutes will be logged off automatically. But every action they perform will reset the window by 30 minutes again. If a hacker manages to gain control over a user’s session without knowing their login, this gives them at least 30 minutes to break into the system.

A more strict security policy disables the sliding expiration and limits the potential exposure time. Again, this is entirely configurable through the web.config file:

<system.web>
    <authentication &hellip;.>
      <forms timeout="30" slidingExpiration="false" />
    </authentication>
</system.web>

There is a caveat here; this setting requires every user to log in again every 30 minutes. Unless you use some kind of SSO-mechanism that can transparently log users in again, this might be quite user-unfriendly.

7. Route HTTP-traffic to HTTPS

It’s good practice to use SSL for enterprise applications, and I will assume that you’re already doing this. But how do you make users use the HTTPS version? One approach is to educate users to only access the website through it’s https:// address, and disabling http altogether. This is certainly the most secure. A more user-friendly approach is to redirect all visitors coming in through HTTP to HTTPS, so that any consecutive call comes in through HTTPS instead. If you have the URL Rewrite 2.0 module installed on IIS (7+) you can add the following to your web.config:

  <!-- Configure SSL URL Rewriting and IP filtering to block unwanted users -->
  <system.webServer>
    <rewrite xdt:Transform="Insert">
      <rules>
        <rule name="HTTP to HTTPS redirect" stopProcessing="true">
          <match url="(.*)" />
          <conditions>
            <add input="{HTTPS}" pattern="off" ignoreCase="true" />
          </conditions>
          <action type="Redirect" redirectType="Found" url="https://{HTTP_HOST}/{R:1}" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>

This strategy can be augmented with HSTS headers. These force (most) supporting browsers to exclusively communicate with the server over encrypted connections even if unencrypted connections are available. You can enable this header by adding the following to your Global.asax file:

protected void Application_BeginRequest(Object sender, EventArgs e)
{
    switch (Request.Url.Scheme)
    {
        case "https":
            Response.AddHeader("Strict-Transport-Security", "max-age=31536000");
            break;
        case "http":
            var path = "https://" + Request.Url.Host + Request.Url.PathAndQuery;
            Response.Status = "301 Moved Permanently";
            Response.AddHeader("Location", path);
            break;
    }
}

8. Invalidate sessions when users log off

FormsAuthentication and SimpleMembership (obviously) allow users to log off. Although this removes authentication cookies from their computer, it does not invalidate the session on the server. When a hacker managed to extract the authentication cookie, it can be used to continue using the server even when the compromised user has logged off. One way to mitigate the risk is to disable the sliding expiration and set a short expiration timeout on the authentication cookie. The server will remove the session once it expires. But it still feels like a (albeit strange) security hole.

FormsAuthentication offers no server-side invalidation mechanism out of the box that I know. One approach is to roll your own authentication mechanism or find another one. But this is difficult when you are already tied to FormsAuthentication. A simpler approach is to set up a double bookkeeping where you keep a list of active sessions. When a user logs in, you register the active session in that list. You remove it again when they log off. You then have to add a check to see if the current user’s session is known in your list, either with a Global.asax check or a special Attribute. It’s a bit of work, but it closes a potential hole.

9. Don’t use external libraries

Many developers use external libraries to augment their website, like Bootstrap or jQuery. Instead of copying the libraries to your website it’s more convenient to just reference libraries hosted by Google or some other provider. This way you also benefit from updates. Although I don’t think there is a very serious risk here, penetration tests and PCI-compliancy checks don’t like external content. External libraries can be compromised and used to attack people using your website with viruses, malware or targeted attacks. The same problem applies to Google Analytics, external fonts and stylesheets. The best approach is to simply copy these libraries to your website instead.

10. Add Content Security Policy headers

Despite all the aforementioned strategies, the most vulnerable part of the application is still the user’s browser. No level of security is going to help if a hacker manages to execute code (javascript or malware) in that browser.

A Content-Security-Policy can help tighten the level of security that the user’s browser enforces on your website. You can define a policy to make the user’s browser reject scripts, stylesheets and content from other websites altogether, hereby limiting the options of a hacker to inject malicious content.

A Content-Security-Policy can be added as a set of headers. The easiest approach is to add a filter that you can execute on every pageload (in the Global.asax):

    public class ContentSecurityPolicyFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var response = filterContext.HttpContext.Response;
            const string value = "default-src 'self&rsquo;&rdquo;;

            response.AddHeader("X-WebKit-CSP", value);
            response.AddHeader("X-Content-Security-Policy", value+"; options eval-script");
            response.AddHeader("Content-Security-Policy", value);
            base.OnActionExecuting(filterContext);
        }
    }

You obviously have to tweak the actual policy (the one above is very limiting) to suit your needs. CSP allows a great number of filtering options and even allows you to specify different options for files, stylesheets, images, scripts and fonts. You can read more about there here.

11. Enforce password protection policies

Even if your website and webserver are very secure, a weak password compromises  everything. It’s no wonder that guessing passwords is a favorite past-time for hackers. And if this guessing is automated with dictionaries and unrestricted retries it will eventually pay off.

Your application should employ a password policy that enforces a minimum length and strength of passwords. It can also enforce expiration, where users have to change their password every few months. But I’m not a fan of forcing users to come up with strong and random passwords periodically. We’re not computers, so most people just write down their strong, random password on a memo or keep it in a text file. Especially when the password has to be changed frequently. Even so it’s still a good idea to help users come up with strong passwords. You can offer suggestions and examples of high-entropy passwords that are still easy to remember (like ‘15bottlesinagreenbarwith9peopleand1singer’) or indicate the password strength. It’s also wise to limit the number of consecutive failed login attempts, or introduce delays, to foil automated password guessing attacks. 

12. Disable SSLv2, SSLv3 and other weak protocols / ciphers

A highly secure website is pointless if the server itself is not secure. We’ve recently witnessed a number of serious threats (HeartBleed, PoodleBleed) that demonstrate how easily a hacker can break into insecure or weak SSL connections. The strongest (and recommended) levels of SSL-encryption iare TLS1, TLS 1.1 and TLS 1.2. Older versions, like SSLv2 and SSLv3, are insecure and should be disabled. In addition, a number of encryption ciphers are weak and should be disabled as well.

Sadly, this is not a matter of opening some configuration windows. Most websites offer registry edits (or .reg) files to do this, but I find it easier to use the IIS Crypto tool that you can download here. Run the tool on the server and select either the PCI or ‘best practices’ profile to beef up SSL-security.

It’s a good idea to verify your server afterwards with this excellent online SSL Test tool.

Conclusion

I started off with the observation that MVC is pretty secure out of the box. And despite the length of this post, it really is. But passing a penetration test or a PCI-compliance scan requires augmented security that is not enabled by default. This post offers a number of (relatively easy) changes that really beef up security.

But always remember that your website is first and foremost there for the users. A very secure website that is highly user-unfriendly will simply not work. So find a balance between security and usability, depending on the sensitivity of your application.

Christiaan Verwijs
Christiaan Verwijs

Scrum Master, Trainer, Developer & founder of Agilistic