Image for post
Image for post

12 strategies to quickly harden your .NET webapplication

Originally published at blog.agilistic.nl on January 29, 2015.

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

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

<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

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

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.

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="true" />
<authentication>
<forms requireSSL="true" />
</authentication>
</system.web>

5. Limit the chattiness of your application

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

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

<!-- 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;
}
}

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

10. Add Content Security Policy headers

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

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

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

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.

Image for post
Image for post
You can already support us with $1/month. Find out more on patreon.com/liberators

Written by

I liberate teams & organizations from de-humanizing, ineffective ways of organizing work. Passionate developer, organizational psychologist, and Scrum Master.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store