So you want to secure your Umbraco site

Imagine, if you will, for a second, that you're trying to secure your Umbraco site by running it over an https connection, sounds complicated? Not so much. It's fairly trivial to set up once you have a certificate, which you now know how to create using Let's Encrypt.

I'm not going to explain how to create a binding in IIS, this process is different per hosting provider and it's impossible to cover this for everyone. We've made it easy on Umbraco as a Service (UaaS): upload a .pfx file, bind it to one of your host names. We can (and will) make it even easier.

Alright, we're there, our site, when typing "https://" as the prefix works and gives us a satisfying green lock to show that the connection was encrypted end to end and Chrome tells us our site is secure, yay!

There's only one more thing that you need to do for Umbraco to make sure that the backoffice is secure: go into your web.config and find the appSetting called "umbracoUseSSL". This setting is "false" by default and needs to be changed to "true". A quick look in the source code of Umbraco teaches us that the only use for this setting is to make sure that the cookies issued when logging into the backoffice get the "secure" flag, meaning it will only send the cookie when the connection is encrypted (so only over https).

This is all it does, and that's all there is to it, you've secured your Umbraco site!

Taking it further

We're running successfully on https, but we might not be at the peak of our game yet, let's run some online scans to see what else we can do to make our site more secure.

SSLLabs

Okay, that was the good news. Full of hope you run over to SSLLabs to test your site's security. Depending on your hosting server's setup you might and come up with a.. disappointing grade. Luckily, my site lives on Umbraco as a Service, so I get a mighty fine "A" grade.

Hoever, you might end up with a different grade if the server your hosting on has enabled insecure protocols and encryption ciphers. If you're self-hosting then I recommend you run Nartac IIS Crypto and apply the fixes suggested using the "Best Practices" button. It's a dead-simple, one-click fix for most (if not all) of your bad grades on SSLLabs.

HTTPS by default

For the following modifications, I'm going to assume that the URL Rewrite module for IIS is installed on your webhosting server (exactly why Microsoft doesn't ship with this installed by default is beyond me!).

Now that we can access the site over HTTPS, let's redirect all traffic to the site to HTTPS, that way you always have that happy green lock in your browser and you will always encrypt all traffic against people trying to snoop on you whether they are a Man In The Middle, your internet provider or the NSA. This can be done using a URL Rewrite rule. In the following rule "localhost" is excluded from rewriting so that I don't have to jump through hoops to set up valid certificates and hostnames when debugging my site on my local machine.

This configuration goes into the system.webServer/rewrite/rules section of the web.config:

 <rule name="HTTP to HTTPS redirect" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" ignoreCase="true" />
<add input="{HTTP_HOST}" pattern="localhost" negate="true" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
</rule>

While we're adding redirects, it's good for search engines to have just one domain to look at, so I can set up a redirect that strips "www" from any requests (which will then feed into the rule above and makes sure to redirect to HTTPS):

 <rule name="Strip www. from URL" stopProcessing="true">
<match url="^(.*)$" ignoreCase="true" />
<conditions logicalGrouping="MatchAll">
<add input="{HTTP_HOST}" pattern="^www\.(.+)$" />
</conditions>
<action type="Redirect" url="http://{C:1}/{R:1}" redirectType="Permanent" />
</rule>

To further minimize any attacks where bad guys might want to trick you into using insecure HTTP request, you can send up a header with each request called the HTTP Strict Transport Security (HSTS) header. Enabling HSTS will tell the browser: for the specified amount of time you will not look up any pages on this domain over HTTP any more, always use HTTPS. This is an addition that can be made to the system.webServer/rewrite/outboundRules section:

 <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" />
<add input="{HTTP_HOST}" pattern="localhost" negate="true" />
</conditions>
<action type="Rewrite" value="max-age=63072000; includeSubDomains; preload" />
</rule>
</outboundRules>

This adds the "Strict-Transport-Security" header that tells browsers: for the next 63072000 seconds (which is two years) the browser should not make any HTTP requests to this domain.

Note: the rules for HSTS inclusion might change from time to time. Make sure to read the current requirements before you follow these steps.

However... there's still a tiny sliver of an attack vector here. If you are a Man in the Middle and manage to lure someone to a site that they've never visited before, their very first request will still be the only one ever to go over HTTP. In that single request the Man in the Middle could still possibly do bad things. The only way to eliminate this risk is to never allow HTTP connections to the site, but then everybody needs to know that you can only ever get to the site by prefixing it with "https://". Not very user friendly.

You can, however, ask to be put on a list that is baked into browsers like Chrome, Firefox, Safari, IE11 and Edge. This is called HSTS Preloading and takes a few weeks to get set up (it's a manual process). When you do finally make it on this preloaded list, your browser will never request any pages over HTTP but choose HTTPS by default. Part of the manual check that Google will do is to see if HSTS is set up including subdomains and if the preload parameter is there, this is why they're added to the rewrite rule above.

ASafaWeb

The "Automated Security Analyser for ASP.NET Websites" will test your Umbraco site for known issues with ASP.NET websites. I'm not doing so well on this one and have a few things to fix.

AsafaWeb gives me excellent guidance to fix things like Custom Errors and an exposed Stack Trace, just update the web.config to set Custom Errors to "RemoteOnly" and we're good.

As for the orange warnings:

  • Excessive headers: The header "Server: Microsoft-IIS/8.5" gets sent with each response. I have tried disabling this but apparently our UaaS servers forcefully add this header. Nothing I could do about it, this needs to be removed at a server level. There's many other ways to probe sites and find out (by looking at their behavior) that it's running IIS and even which version it runs. So attackers specifically out to target my site are only a few seconds extra delayed in picking the correct attack vectors, I'm not worried about this header.
  • HTTP only cookies: The "ARRAffinity" cookie is only there for IIS to quickly determine on which of the available web servers my website lives. It's not an attack vector: if it's wrong or doesn't exist, this cookie will just be overwritten with a new one.
  • Clickjacking: A valid concern, I can deny people framing my site with the simple addition of "X-Frame-Options" to the web.config (more on this later!).
    Note: Always make sure you remove a custom header first, if the webserver already has it's own "add" rule, then you can't overwrite it by inserting your own, you need to remove the existing one first. Also note that Umbraco has tried to be helpful and removed the header that tells the world what MVC version you're running by removing "X-Powered-By" in the systemWebserver/httpProtocol/customHeaders section of your web.config.
     <httpProtocol>
    <customHeaders>
    <!-- Ensure the powered by header is not returned -->
    <remove name="X-Powered-By" />
    <remove name="X-Frame-Options" />
    <add name="X-Frame-Options" value="DENY" />
    </customHeaders>
    </httpProtocol>

There's a few gray boxes there: because my site doesn't have a view state in the HTML, AsafaWeb assumes (correctly) that I'm not using WebForms, so those tests didn't need to run any further. I couldn't figure out how to trigger the "Hash dos patch" test, even after adding a form that does a POST (as described on AsafaWeb) it doesn't test for this problem. Luckily I know that UaaS runs on servers not affected by the MS11-100 security vulnerability, but you might want to check with your hosting provider.

Looks better now:

Security-headers.io

Going even further down into securing our website, there's some "fun" things we can do to make most websites misbehave, like making it do the Harlem Shake.

Security-headers.io looks to see if you've implemented policies to mitigate these kinds of problems which are mostly XSS (cross site scripting) based. Look at this result.. ouch:

We can easily makes this a lot better by following some of the advise here on adding a "X-Xss-Protection" and a "X-Content-Type-Options" header:

 <httpProtocol>
<customHeaders>
<!-- Ensure the powered by header is not returned -->
<remove name="X-Powered-By" />
<remove name="X-Frame-Options" />
<add name="X-Frame-Options" value="DENY" />
<remove name="X-Xss-Protection" />
<add name="X-Xss-Protection" value="1; mode=block" />
<remove name="X-Content-Type-Options" />
<add name="X-Content-Type-Options" value="nosniff" />
</customHeaders>
</httpProtocol>

Better:

The Content Security Policy (CSP) is a lot harder to implement because it requires you to look at all of your site's assets and whitelist them. This is difficult especially if you load video's from YouTube, use CDN hosted javascript libraries, links to external images etc. Which brings us to the following check to run.

CSP Analyser

The CSP analyser over at report-uri.io looks at any policies you've implemented and tells you how good they are. It's impossible to give a good policy for all websites, so I'll just post the one I've struggled with and finally landed on for this site:

 <httpProtocol>
<customHeaders>
<!-- Ensure the powered by header is not returned -->
<remove name="X-Powered-By" />
<remove name="X-Frame-Options" />
<add name="X-Frame-Options" value="DENY" />
<remove name="X-Xss-Protection" />
<add name="X-Xss-Protection" value="1; mode=block" />
<remove name="X-Content-Type-Options" />
<add name="X-Content-Type-Options" value="nosniff" />
<remove name="Content-Security-Policy" />
<add name="Content-Security-Policy" value="default-src 'self' https://www.gravatar.com;script-src 'self' https://www.google-analytics.com https://ssl.google-analytics.com;style-src 'self' 'sha256-MZKTI0Eg1N13tshpFaVW65co/LeICXq4hyVx6GWVlK0=' 'sha256-CwE3Bg0VYQOIdNAkbB/Btdkhul49qZuwgNCMPgNY5zw=' 'sha256-LpfmXS+4ZtL2uPRZgkoR29Ghbxcfime/CsD/4w5VujE=' 'sha256-YJO/M9OgDKEBRKGqp4Zd07dzlagbB+qmKgThG52u/Mk=' https://fonts.googleapis.com;img-src 'self' data: https://www.gravatar.com https://www.google-analytics.com;font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com;" />
</customHeaders>
</httpProtocol>

I am using Gravatar images, Google Analytics and the Google Fonts API. The sha256 references are there to fix some things that Modernizr.js wants to execute, which wouldn't otherwise be allowed, Chrome dev tools will tell you exactly what to add if this is a problem for you:

To make it a bit easier for you to manage, report-uri.io allows you to set up a free account. Using that, all CSP violations will be logged for you so you can have a look at updating your whitelist accordingly.

After setting up a CSP, securityheaders.io now reports a respectable "A" grade.

I've looked into Public Key Pinning (HPKP) but the process seems too onerous for little gain for now. The problem with HPKP is currently that I don't understand how backup CSRs are supposed to work and what exactly I need to do when my current certificate expires. I have done some experiments and they worked but I need to do further testing to see what it will take to switch to a new certificate.

In case you are wondering (and are brave), the HPKP header can be configured like so in system.webServer/httpProtocol/customHeaders:

 <remove name="Public-Key-Pins" />
<add name="Public-Key-Pins" value="max-age=31536000; pin-sha256=&quot;h2EVK2+bga6XAxu7ImUQM0PJsgZd2/a2VtlcSmV87s4=&quot;; pin-sha256=&quot;9ilXj1leytbsCvXVIFJ1uzjmej2bzs05qzRzmfFzXKs=&quot;; pin-sha256=&quot;nupZBiNmjIMxIyEll+OBYjvMORUEyYTTr7K5bE2z7L0=&quot;;" />

Note that the double quotes need to be escaped because the web.config file is an XML file, so replace " with &quot; everywhere in the value of this header.

Back to Umbraco

Now that we've made our frontend all nice and safe, let's go back into the backoffice of Umbraco.

Whoops, we broke it!

There's a few things going on in the backoffice that we need to allow now that we've disallowed a lot of them on the frontend. Umbraco still uses iframes for some pages in the backoffice so we'll need to allow those. The Content Security Policy is also blocking a lot of asset loading because they're set pretty strict on the frontend.

Luckily we don't have to change our frontend setup, we can just change the backoffice requirements a little bit. All the way at the bottom of our web.config we already have a <location path="umbraco"> section which tells IIS: for this location (the umbraco path) we want to apply different rules then for the rest of the site. We can amend this section with a custom CSP and allow frames from the same origin (so only frames with a location that lives somewhere in our site).

We're already disabling urlCompression for the backoffice as that can conflict with our backoffice javascripts, so let's add our updated headers there:

 <location path="umbraco">
<system.webServer>
<urlCompression doStaticCompression="false" doDynamicCompression="false" dynamicCompressionBeforeCache="false" />
<httpProtocol>
<customHeaders>
<remove name="X-Frame-Options" />
<add name="X-Frame-Options" value="SAMEORIGIN" />
<remove name="Content-Security-Policy" />
<add name="Content-Security-Policy" value="default-src 'self' www.gravatar.com player.vimeo.com *.vimeocdn.com packages.umbraco.org our.umbraco.org;script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline';img-src 'self' data: www.gravatar.com umbraco.tv;font-src 'self';" />
</customHeaders>
</httpProtocol>
</system.webServer>
</location>

Much better, our backoffice is back without errors.

One interesting thing I found when implementing CSP rules is that I was not allowed to have inline CSS in my site, this is a good thing, I don't want inline CSS, I want everything to be nicely tucked away in a CSS file.

One problem though: the rich text editor. When you insert an image in the RTE, Umbraco automatically adds an inline style for you with the dimensions of the image and there seems to be no way to prevent it from doing so. 

I've created a simple extension method that goes through your html and strips out those inline styles. This StringExtensions.cs can be dropped into your App_Code folder:

using System.Web;
using HtmlAgilityPack;

namespace Cultiv.StringExtensions
{
public static class RteStyles
{
public static IHtmlString RemoveInlineImageStyles(this string text)
{
var htmlString = new HtmlString(text);
return htmlString.RemoveInlineImageStyles();
}

public static IHtmlString RemoveInlineImageStyles(this IHtmlString htmlString)
{
var htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(htmlString.ToString());
if(htmlDocument == null || htmlDocument.DocumentNode == null || htmlDocument.DocumentNode.SelectNodes("//img[@style]") == null)
{
return htmlString;
}
else
{
foreach (var node in htmlDocument.DocumentNode.SelectNodes("//img[@style]"))
{
var attribute = node.Attributes["style"];
node.Attributes.Remove("style");
}
}
return new HtmlString(htmlDocument.DocumentNode.OuterHtml);
}
}
}

I use it as follows in my templates:

@(Model.Content.GetPropertyValue<string>("bodyText").RemoveInlineImageStyles())

Conclusion

Security is hard. :-)

Luckily there's plenty of tools that help ease the pain. We are always looking into updating Umbraco where possible to take away the pain by setting up sensible defaults. We're also working on making things easier to set up on Umbraco as a Service where we can rely more on automation.

There's a few security related sites I should point to that are excellent in helping you understand security and keeping you safe:

Finally: there's a lot more you can do to protect your site, but this is a mammoth post already so I'll end this here in hopes that I get more time in the future to cover related topics.

 

Sebastiaan Janssen

Dutch guy living in (and loving) Copenhagen, working at Umbraco HQ. Lifehacker, skeptic, music lover, cyclist, developer.

 

14 comments on this article

Avatar for Josh Josh | March 26 2016 02:30
This is great stuff mate- thanks for your time to write it up, came together really well!

Avatar for James Jackson-South James Jackson-South | March 29 2016 05:53
This is an excellent blog post! I'll be referring to it for every release now.

Been thinking about the umbracoUseSSL setting. I usually use HttpContext.Request.IsSecureConnection to determine whether the Secure property should be set. That's one less thing to forget.

Avatar for Mark Drake Mark Drake | March 31 2016 14:14
I can't imagine the time it took you to go through all of this - thank you. All of this material is great and will become part of our checklist prior to launches. Great info, tools, and processes. Thanks!

Avatar for Jeffrey Schoemaker Jeffrey Schoemaker | April 1 2016 08:26
Hi Sebastiaan, great stuff and I think it sums up the basic checks you should do pretty good, thanks for writing it down!

Please be aware that including "includeSubDomains" in your HSTS-header can do some serious harm if you're working for a company that isn't aware if there exists any other subdomains that you don't control or run on https! These sites will immediately break when adding this part of the header.

Avatar for Jeffrey Schoemaker Jeffrey Schoemaker | May 11 2016 07:24
One more thing that we discovered in our projects; In MVC5 the framework is adding the X-Frame-Options-header itself when you include an anti-forgery-token. During our security tests we discovered that sometimes there were headers with the content of SAMEORIGIN, SAMEORIGIN, SAMEORIGIN, SAMEORIGIN. This was because the header was once inserted by the server (through the configuration mentioned in the blog) and three times due to three forms with antiforgerytoken.

Every browser will work correctly with this setting but our security tests failed.

This issue can be fixed by adding 'AntiForgeryConfig.SuppressXFrameOptionsHeader = true;' to your Global.asax.cx. More info on: https://www.quppa.net/blog/2013/11/28/html-antiforgerytoken-sets-an-x-frame-options-header-with-the-value-sameorigin/

Cheers!

Avatar for David Peck David Peck | July 21 2016 13:26
I already told you at CG but this is a really useful post. Thanks!

I thought it worth mentioning my experience with some Safari quirks and CSP. If you're still running on http, then you'll need to specify https://... for any domains loaded over https. Also, Safari doesn't support nonce, and so you'll need to specify inline-script as a fallback (http://stackoverflow.com/questions/32788355/csp-nonce-ignored-by-safari)

Avatar for Jon Humphrey Jon Humphrey | September 19 2016 16:32
Thanks Sebastiaan,

This whole article, and subsequently linked urls, have given me a much better understanding of the whole security side of things for an Umbraco site! :-D

Not to mention I can proudly say my site is now grade A secure ... albeit with small caveats! ;-)

#H5YR!!!

Avatar for Jes Mandrup Jes Mandrup | October 11 2016 09:23
Great article.

What are the options if we want to to encrypt the database? In our case the members can post comments and we stored their phonenumbers.

Avatar for Mark Wemekamp Mark Wemekamp | January 4 2017 14:39
Hi Sebastian,
Thanks for the great article. I've tried adding the X-Frame-Options header with the DENY value, but the backend wouldn't let me add new dictionary items. Changing the value to SAMEORIGIN fixed this

Avatar for David Peck David Peck | January 4 2017 14:42
I think the <location path="umbraco" /> node mentioned in the article might be the bit you're missing. That sets SAMEORIGIN just for the Umbraco bit.

Avatar for Mark Wemekamp Mark Wemekamp | January 4 2017 14:47
Thanks for the heads up David, I missed that part.

Avatar for Jack Math Jack Math | March 10 2017 21:39
A brilliant post! I have been trying to implement this on a 6.2.6 installation with legacy XSLT. I get stuck with errors within RTE when editing. There is a long list of errors and the Update or Cancel buttons when using view as HTML in RTE don't fire. You have a script for the template to hid inline CSS generated by RTE but this is for MVC, is there a way around when templates are using xslt?

Avatar for Bille Bille | July 6 2017 12:48
Really, really awesome post. Thanks.

Avatar for Morten Empeño Morten Empeño | September 24 2017 09:00
Thank you so much for doing such a great work, and for sharing all your findings.

So much value and so well explained and illustrated :)