by Matthew Thornton
11 July, 2019 - 7 minute read

The potential cost to businesses of hackers loading malicious scripts onto their websites has been demonstrated by the huge £183 million ($228 million) fine meted out to British Airways for failing to protect customer data from the attack. This type of attack, which also carries the risk of reputational damage, is more generally known as Cross-Site scripting (XSS).

Information security requires continual investment and Winton is cognisant of the growing sophistication of adversaries. Recently, our technology department received hands-on training from security researcher Scott Helme. A key lesson learned was that many XSS attacks can be prevented with a good Content Security Policy (CSP).

A CSP, which is a computer security standard produced by the World Wide Web Consortium, instructs the browser to only load scripts from sources that the policy defines. While the concept is simple, writing a policy is notoriously tricky, particularly when retrofitting it to an existing application. As a consequence, we have sought to formulate a simple policy that can work for all of our applications.

In the first part of this blog post, we show that by using some of the new features available in CSP Level 3 it is possible to write a robust yet simple policy that can be used for many applications. In the second part, we demonstrate how we have used open source libraries to implement the policy for a React application hosted on ASP.NET Core.

CSP 3 FTW

Before CSP Level 3 was introduced, it was typical for a policy to define all of the sources from which scripts were being loaded. For example, a policy might have been defined as:

script-src 'self' 'example.com' 'cdnjs.com';

This policy says: “Allow any scripts to run that are loaded from our own server, example.com or cdnjs.com.” Not only is this approach brittle, but it also trusts the entirety of the example.com and cdnjs.com domains meaning it is only as secure as those third-party domains.

With the new strict-dynamic directive in CSP Level 3, we can both simplify and improve the security of the policy. Google’s guidance on strict CSPs makes use of this and their recommended policy is:

script-src 'nonce-{random}' 'strict-dynamic' 'unsafe-inline' https: http:;

Let’s break this down to see how it works. A nonce is a value that is generated randomly by the server for each request. If the nonce was generated with the value xenlsFUxqZbACfP4A0QZ, then the server would add 'nonce-xenlsFUxqZbACfP4A0QZ' to the script-src of the CSP. Any script tags in the HTML would then need to have the nonce attribute set on them in order to be allowed to run:

<script type="text/javascript" nonce="xenlsFUxqZbACfP4A0QZ">
</script>

To inject a script, an attacker would have to guess the random nonce and add it to their script tag.

It is common in a modern web application to have an entry point script that loads other scripts as and when required. strict-dynamic was designed to cater for this situation, as it instructs the browser to trust any transitive scripts as long as they are loaded by a script with a valid nonce. The other directives are there for compatibility with older browsers that do not support CSP Level 3.

The key point to note here is that there is no longer a domain whitelist in the script-src. This policy is application agnostic, which means that we can use it in all of our applications without modification.

Disclaimer: We’ve only shown the script-src section here, since that part is generally the trickiest. For final production, there are a few more sections that should be set, such as style-src. See the useful links at the bottom for free tools that can help ensure you have a complete policy.

The implementation

Our web applications are typically bundled using webpack and hosted using ASP.NET Core. So how do we implement our desired CSP in this type of application?

Step 1: Generate the policy in ASP.NET Core

We can use the excellent NWebSec libraries to do most of the server side work in ASP.NET Core. In particular the NWebSec.AspNetCore.Middleware library defines ASP.NET Core middleware that can set important security headers, including a CSP. To generate our recommended policy we simply need to call UseCsp in the Configure method of the Startup class with the following options:

app.UseCsp(
    options => options
        .ScriptSources(
            s => s
                .StrictDynamic()
                .CustomSources("https:", "http:")
                .UnsafeInline()
                .UnsafeEval(env)));

Note that in the development environment we want to use webpack’s hot module replacement, but this requires the use of eval(), which a CSP will block unless the unsafe-eval directive is added. Allowing unsafe-eval would weaken our policy, so we define an overload of UnsafeEval that only adds it in the development environment, which looks like this:

private static ICspDirectiveConfiguration UnsafeEval(
    this ICspDirectiveConfiguration configuration,
    IHostingEnvironment env)
    => env.IsDevelopment() ? configuration.UnsafeEval() : configuration;

Step 2: Adding nonces to scripts

If you were paying close attention, you may have noticed that step 1 contains no mention of nonces. This is because NWebSec will automatically add the nonce to the script-src, if we add one to a script. The nws-csp-add-nonce tag helper, defined in NWebSec.AspNetCore.TagHelpers, can be used in a .cshtml file to add nonces to both inline and external scripts:

// Bring the required tag helpers into scope
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, NWebsec.AspNetCore.Mvc.TagHelpers

<script nws-csp-add-nonce="true">
    // an inline script
</script>

<script type="text/javascript" src="/dist/app.js" nws-csp-add-nonce="true"></script>

Step 3: Making webpack play nice

The final step is to configure webpack correctly. Our two goals here are as follows:

  1. Adhere to webpack’s caching guide for JavaScript assets
  2. Add nonces to all of the scripts that need to be included on our web page.

To do this, we used the following Webpack config file (abbreviated to show key sections):

const ChunkRenamePlugin = require('webpack-chunk-rename-plugin');

const BUILD_PATH = path.resolve(__dirname, 'wwwroot/dist');

module.exports = {
    output: {
        filename: '[name].js',
        chunkFilename: '[name].[contenthash].js',
        publicPath: '/dist/'
    },
    plugins: [
        new webpack.HashedModuleIdsPlugin(),
        new ChunkRenamePlugin({
            initialChunksWithEntry: true,
            'vendors~main': '[name].min.js'
        })
    ],
    optimization: {
        runtimeChunk: 'single',
        splitChunks: {
            chunks: 'all'
        }
    }
};

The main difference from webpack’s recommendation is that we are not adding hashes to the entry point script file names. We need their file names to be static so that we can reference them in the Index.cshtml file and then add the nonces. These scripts load all of the other chunks transitively, so strict-dynamic has us covered. Instead, we use the asp-append-version tag helper to get cache-busting on these entry points. To reference these scripts, we just add the following code to Index.cshtml:

<script type="text/javascript" src="/dist/runtime.js" asp-append-version="true" nws-csp-add-nonce="true"></script>
<script type="text/javascript" src="/dist/vendors~main.min.js" asp-append-version="true" nws-csp-add-nonce="true"></script>
<script type="text/javascript" src="/dist/main.js" asp-append-version="true" nws-csp-add-nonce="true"></script>

With that, we’re done. We’ve attached nonces to all of the scripts tags while still following webpack’s guidance on good caching practices.

Note: In webpack 4 if optimiszation.runtimeChunk is set to single, as we have it, then webpack treats the vendors bundles as a non-entry script and it gets named using the output.chunkFilename strategy. There is an issue tracking this, but we have been using the webpack-chunk-rename-plugin as a workaround.

Bonus Step: Adding a nonce to the Application Insights script

If you’re using Application Insights to gather client-side telemetry, then you are probably adding the following code to your .cshtml files to inject the script file:

@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet

<!DOCTYPE html>
<html>
<head>
@Html.Raw(JavaScriptSnippet.FullScript)
</head>
<html>

This is convenient, but how do we add a nonce to the generated script tag? To do this, we used HtmlAgilityPack to re-write the script tag and add the nonce:

@using HtmlAgilityPack;

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" nws-csp-add-nonce="true">
    @Html.Raw(string.IsNullOrWhiteSpace(AppInsightsJavaScriptSnippet.FullScript) ?
        string.Empty :
        HtmlNode.CreateNode(AppInsightsJavaScriptSnippet.FullScript).InnerHtml)
</script>
</head>
<html>

Wrapping up

As we have seen, using new CSP features such as strict-dynamic and nonces can produce a single secure policy that works for most web applications. Implementation in ASP.NET Core is also only a few lines of code, thanks to existing open-source libraries. It extends beyond React applications and will work for any JavaScript-based framework that can be bundled with webpack.