How to implement Content Security Policy (CSP) in ASP.NET MVC

How to Implement Content Security Policy (CSP) in ASP.NET MVC

I am a developer at Shopless online marketplace and I regularly run a Google Lighthouse report on our website to check if we are complying with software best practices. Last night when I ran the report I noticed that we are getting a new warning:

Ensure CSP is effective against XSS attacks

What Is Content Security Policy (CSP)

According to Mozilla.org

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. 

To enable CSP, you need to configure your webserver to include the content-security-policy HTTP header in the response. CSP header tells the browsers which domains are the trusted domains for the resources that you include in your page. For example if you want to include jQuery library from Cloudflare CDN in your page, you would need to explicitly include cdnjs.cloudflare.com in your CSP Header.

Implementing CSP in ASP.NET MVC

Shopless is developed in ASP.NET MVC, so we needed a solution for this Framework. NWebsec.MVC Nuget package, is an excellent package which support CSP header and we decided to use it for this purpose. The first thing that you need to do is to install NWebsec.MVC.

If you read NWebsec CSP configuration page, you will notice that they provide 3 options for implementing CSP Header:

  1. Sources in web.config
  2. Configuring CSP through MVC attributes
  3. Configuring CSP middleware

We decided to go with the first option, and here I am going to explain our solution. The reason we decided to go with this option is that it was the easiest choice, it did not require any modification to our source code and all the configuration where managed in one place, web.config.

Solution Using Web.Config (that we chose to use)

Once you install NWebsec.MVC package you will notice that it has added a new sections into your web.config. The section that we are interested in is <securityHttpHeaders> (note that <nwebsec> element is directly added under <configuration> element).

Here is an example of how we implemented our CSP rules:

<nwebsec>
  <httpHeaderSecurityModule xmlns="http://nwebsec.com/HttpHeaderSecurityModuleConfig.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="NWebsecConfig/HttpHeaderSecurityModuleConfig.xsd">
    <securityHttpHeaders>
      <content-Security-Policy enabled="true">
        <default-src self="true"/>
        <script-src self="true">
          <add source="https://cdnjs.cloudflare.com"/>
          <add source="*.googleapis.com" />
          <add source="*.googletagmanager.com" />
        </script-src>
        <style-src unsafeInline="true" self="true">
          <add source="https://cdnjs.cloudflare.com"/>
          <add source="*.googleapis.com" />
        </style-src>
        <font-src self="true" >
          <add source="https://cdnjs.cloudflare.com"/>
          <add source="*.googleapis.com" />
          <add source="*.gstatic.com" />
        </font-src >
        <img-src self="true">
          <add source="data:" />    <!-- to allow inline svg (data:image/svg+xml)-->
          <add source="*.gstatic.com" />
          <add source="*.googleapis.com" />
          <add source="*.w3.org"/>  <!-- referenced for svg images -->
        </img-src>
        <media-src none="true" />
        <frame-src none="true" />
        <connect-src none="true" />
        <object-src none="true" />
        <frame-ancestors none="true" />
        <report-uri enableBuiltinHandler="true"/>
      </content-Security-Policy>
    </securityHttpHeaders>
  </httpHeaderSecurityModule>
</nwebsec>

As you can see we have defined different trusted domains for different types of resources such as Scripts, Styles, Fonts, etc. The default-src would be used for any resource type which is not explicitly define.

Potential Error

Once you implement the above solution, you might run into errors like this:

Refused to load the font ‘https://fonts.gstatic.com/s/orbitron/v17/random.woff2’ because it violates the following Content Security Policy directive: “font-src ‘self’ https://cdnjs.cloudflare.com *.googleapis.com.

The error is self explanatory. If you include a resource in your page, you would need to add its domain as a trusted source in your CSP header.

Gotcha

self does not include subdomains! In the example below, if I serve the CSS from www.my-domain.com, then I would need to explicitly add it as a source, i.e.

<style-src unsafeInline="true" self="true">
  <add source="https://*.my-domain.com"/>  <!-- any subdomain -->
  <add source="https://cdnjs.cloudflare.com"/>
  <add source="*.googleapis.com" />
</style-src>

Another approach (producing similar result)

You can also implement a very similar solution using the <customHeaders> section (which was also added to you web.config by NWebsec). Here is an example:

<httpProtocol>
  <customHeaders>
    <clear />
    <!-- 
    Add your custom header here, for example:
    <add name="Content-Security-Policy" value="default-src 'self'; script-src 'self' https://cdnjs.cloudflare.com; />
    -->
  </customHeaders>
</httpProtocol>

Like the previous approach, this would include the same CSP header in all of the of the server responses, however I find the former approach easier to maintain. See this document if you prefer the later option.

Alternative Solutions (that we didn’t use)

1. Configuring CSP through MVC attributes

Using this option, you can define CSP attributes, something like this:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new CspAttribute());
    filters.Add(new CspDefaultSrcAttribute { Self = true });
}

And then you can annotate your different controllers and actions with these attributes, for example:

[CspScriptSrc(Self = true, CustomSources = "scripts.nwebsec.codeplex.com")]
public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View("Index");
    }

    [CspDefaultSrc(CustomSources = "nwebsec.codeplex.com")]
    public ActionResult Index2()
    {
        return View("Index");
    }

    [CspDefaultSrc(CustomSources = "stuff.nwebsec.codeplex.com")]
    [CspScriptSrc(CustomSources = "scripts.nwebsec.codeplex.com ajax.googleapis.com")]
    public ActionResult Index3()
    {
        return View("Index");
    }
}

If you are interested in this approach, here is an excellent artcile to guide you.

2. Configuring CSP middleware

This option requires NWebsec.Owin Nuget package. Using this option you can define your CSP middleware, for example:

using NWebsec.Owin;

public void Configuration(IAppBuilder app)
{
    app.UseCsp(options => options
        .DefaultSources(s => s.Self())
        .ScriptSources(s => s.Self().CustomSources("scripts.nwebsec.com"))
        .ReportUris(r => r.Uris("/report")));

    app.UseCspReportOnly(options => options
        .DefaultSources(s => s.Self())
        .ImageSources(s => s.None()));
}

And then you can use NWebsec HtmlHelpers in your code, like this:

@using NWebsec.Mvc.HttpHeaders.Csp

<script @Html.CspScriptNonce()>document.write("Hello world")</script>
<style @Html.CspStyleNonce()>
   h1 {
          font-size: 10em;
   }
</style>

Trade-offs

Both of these approaches give you more flexibility over web.config approach. You can define different CSP headers for different webpages, for example, if a page is supposed to show a YouTube video, you can define an attribute to allow *.youtube.com and then annotate that action with the attribute. However both of these approaches require a lot of modifications to your source code which I tent to think is error prone.