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:
- Sources in
web.config
- Configuring CSP through MVC attributes
- 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.