The £183 million fine handed out to British Airways underscores the need for a strong Content Security Policy. Here, we share details of one that Winton has implemented for ASP.NET Core-hosted React applications.
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.
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.
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?
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;
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>
The final step is to configure webpack correctly. Our two goals here are as follows:
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.
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>
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.