How to lock down your CSP when using Swashbuckle
14 Dec 2020Introduction
Swashbuckle is a fantastic .NET library that enables developers to generate Swagger- and OpenAPI-compliant documentation for their APIs.
It also bundles swagger-ui, a tool that allows developers and API consumers to visualise the definition of an API by using the generated JSON OpenAPI document.
Unfortunately, out of the box, swagger-ui uses inline <script>
and <style>
tags, which are considered insecure as they can allow attackers to run arbitrary code in an application through a cross-site scripting (XSS) vulnerability.
See this Google article for more information: https://developers.google.com/web/fundamentals/security/csp/#inline_code_is_considered_harmful
Let’s see how we can work around this issue and keep a strict content security policy that disallows inline tags in our application.
As a note, this post uses the fantastic NetEscapades.AspNetCore.SecurityHeaders
NuGet package developed by Andrew Lock.
It offers a strongly-typed API to work with security-related headers, CSP being one of them.
Check it out at https://github.com/andrewlock/NetEscapades.AspNetCore.SecurityHeaders.
The index.html
file
The main page for swagger-ui is driven by the index.html
file and its CSS and JavaScript resources.
Swashbuckle exposes configuration options for swagger-ui.
Developers can leverage that capability to change the content of the file from one application to another, or simply from one environment to another.
To support this, Swashbuckle maintain their own copy of the index.html
file, in which we can find tokens that get replaced at runtime.
See the several %(<property-name>)
tokens on GitHub: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/98a7dfe0f7ca2aa66c6daa029c3987490bd1eb20/src/Swashbuckle.AspNetCore.SwaggerUI/index.html.
From a CSP perspective, the first two issues are the inline <script>
tag and both the inline <script>
tags.
To remediate those, CSP offers two options:
- Allowing the hashes of the contents of the inline
<script>
and<style>
tags (see https://scotthelme.co.uk/csp-cheat-sheet/#hashes); or - Specifying a
nonce
attribute on the inline tags while allowing it in the content security policy; for obvious reasons, the allowed nonce must be randomly generated and change for every single HTTP request (see https://scotthelme.co.uk/csp-cheat-sheet/#nonces).
Using hashes isn’t viable here as the second inline <script>
tag will contain the JSON representation of our configuration, which could change for each environment we’ll deploy to.
Different values would generate different hashes, all of which we would need to allow in our CSP.
Yuck 😧.
The solution lies in using nonces!
Swashbuckle has an extensibility point which allows to modify the contents of the index.html
file that will be served.
This is done though the SwaggerUIOptions.IndexStream
property.
See https://github.com/domaindrivendev/Swashbuckle.AspNetCore#customize-indexhtml.
The default value is to read the index.html
file embedded in the assembly:
public class SwaggerUIOptions
{
[...]
// See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/89a78db9300a6c3e0854a95ecfc317c9f87c3a8c/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs#L17-L21
public Func<Stream> IndexStream { get; set; } = () => typeof(SwaggerUIOptions).GetTypeInfo().Assembly
.GetManifestResourceStream("Swashbuckle.AspNetCore.SwaggerUI.index.html");
[...]
}
Instead of sending a completely different index.html
file, we’ll override this property to modify the contents dynamically for each HTTP request:
// Startup.cs
public void ConfigureServices()
{
// 1. Register IHttpContextAccessor as we use it below
services.AddHttpContextAccessor();
services
.AddOptions<SwaggerUIOptions>()
.Configure<IHttpContextAccessor>((swaggerUiOptions, httpContextAccessor) =>
{
// 2. Take a reference of the original Stream factory which reads from Swashbuckle's embedded resources
var originalIndexStreamFactory = options.IndexStream;
// 3. Override the Stream factory
options.IndexStream = () =>
{
// 4. Read the original index.html file
using var originalStream = originalIndexStreamFactory();
using var originalStreamReader = new StreamReader(originalStream);
var originalIndexHtmlContents = originalStreamReader.ReadToEnd();
// 5. Get the request-specific nonce generated by NetEscapades.AspNetCore.SecurityHeaders
var requestSpecificNonce = httpContextAccessor.HttpContext.GetNonce();
// 6. Replace inline `<script>` and `<style>` tags by adding a `nonce` attribute to them
var nonceEnabledIndexHtmlContents = originalIndexHtmlContents
.Replace("<script>", $"<script nonce=\"{requestSpecificNonce}\">", StringComparison.OrdinalIgnoreCase)
.Replace("<style>", $"<style nonce=\"{requestSpecificNonce}\">", StringComparison.OrdinalIgnoreCase);
// 7. Return a new Stream that contains our modified contents
return new MemoryStream(Encoding.UTF8.GetBytes(nonceEnabledIndexHtmlContents));
};
});
}
The remaining configuration is done in the Configure
method, to allow nonces for styles and scripts in our CSP:
public void Configure()
{
// Note this sample only focuses on CSP, but you might want to set other security-related headers if you use this library
app.UseSecurityHeaders(policyCollection =>
{
policyCollection.AddContentSecurityPolicy(csp =>
{
// Only allow loading resources from this app by default
csp.AddDefaultSrc().Self();
csp.AddStyleSrc()
.Self()
// Allow nonce-enabled <style> tags
.WithNonce();
csp.AddScriptSrc()
.Self()
// Allow nonce-enabled <script> tags
.WithNonce();
[...]
});
});
}
If we access our swagger-ui page again, everything seems to be working fine! 🎉
If we refresh the page and analyse the HTML code, we can see that the value of nonce
attributes added to the <script>
and <style>
tags changes on every request.
If we open our browser developer tools, the console should show two errors related to loading images through the data
protocol.
Because this protocol is not allowed in our content security policy, the browser refuses to load them.
From what I can see, they’re related to the Swagger logo in the top-left corner and the down-facing arrow in the definition dropdown in the top-right corner.
As I believe the rest is working fine, I’ll let you decide whether you want to allow loading images from data
, which is considered insecure.
If you decide to go ahead, you can use the code below:
public void Configure()
{
app.UseSecurityHeaders(policyCollection =>
{
policyCollection.AddContentSecurityPolicy(csp =>
{
[...]
csp.AddImgSrc()
.Self()
// Allow loading images from data: links
.Data();
[...]
});
});
}
The oauth2-redirect.html
file
Update on 6 July 2022
See the update block at the bottom of this section if you use Swashbuckle v6.0 or greater for an easier solution. You’re still encouraged to read this section to get the necessary context.
The other HTML file you might be using is the oauth2-redirect.html
file, used by default if you decide to implement an authentication flow from swagger-ui.
This can be beneficial as it dramatically eases the discovery of the API endpoints.
At the time of writing, Swashbuckle uses swagger-ui v3.25.0.
If we head over to the swagger-api/swagger-ui
repository, we can find the oauth2-redirect.html
file.
See https://github.com/swagger-api/swagger-ui/blob/v3.25.0/dist/oauth2-redirect.html.
CSP-wise, we have 2 issues:
- It contains an inline
<script>
tag; and - It contains an inline event handler,
<body onload="run">
, which is not supported by Chrome nor the new Edge based on Chromium.
Another point worth noting is that Swashbuckle doesn’t expose a way to override the content of the oauth2-redirect.html
file.
The solution we came up with is to hold a slightly modified copy of the oauth2-redirect.html
file in our application:
<!-- Original file -->
<body onload="run()">
</body>
<script>
function run () {
[...]
}
</script>
<!-- Modified copy -->
<body>
</body>
<script>
function run () {
[...]
}
document.addEventListener('DOMContentLoaded', function () { run(); });
</script>
By putting this copy in the wwwroot/swagger
folder, it’ll then be served by our static files middleware instead of by the Swashbuckle SwaggerUI middleware.
We can then compute the hash of the contents of the <script>
tag, and allow it in our content security policy.
However, this is a suboptimal solution for several reasons:
- We need to maintain a modified copy of a dependency file;
- If we update the Swashbuckle NuGet package, it might come with an updated version of swagger-ui, potentially modifying the hash of the inline
<script>
tag and breaking our CSP.
We’ve tried to mitigate these from different angles.
The first one is to create a suite of tests that ensure the following statements stay true, minimising the possibility of breaking the app:
- We make sure that the hash we allow in our CSP matches the hash of the contents of the modified copy we keep.
If someone updates or deletes the copy we keep, the test will fail.
Because line endings influence the hash of the contents, we leverage both the
.gitattributes
and.editorconfig
files to ensure our copy uses LF line endings, consistent with the source file; - We also make sure that our copy of the file doesn’t “drift” from the one embedded in Swashbuckle’s assembly.
Given we only add a new line at the end of the inline
<script>
tag, we can compare them and assert that one is a superset of the other; - Finally, we have a test to ensure that we still need to keep a copy of the original
oauth2-redirect.html
file that moves from using inline event handlers to a solution based on theDOMContentLoaded
event.
On that last point.
We opened a pull request on the swagger-api/swagger-ui
repository to move away from the inline event handler.
While that PR was merged and the change was released in swagger-ui v3.34.0, it hasn’t flowed down to Swashbuckle.
The hope is that at some point in the future, Swashbuckle will update swagger-ui, we’ll update Swashbuckle, the test will fail, and we’ll be able to remove our modified copy of the file. 🤞 it works out!
Update on 6 July 2022
Swashbuckle, starting with v6.0, is now bundling a version of swagger-ui that contains the change mentioned above in the pull request we opened.
This means that we no longer need to maintain a separate version of the
oauth2-redirect.html
file. However, because the file still contains an inline<script>
tag, we need to allow its hash in our CSP.It greatly reduces the complexity of the solution, though, as we now only need to ensure that the hash we allow is the correct one, which we do via an automated test.
Conclusion
In this post, we explored how we can keep a fairly strict content security policy in our app while leveraging the great tools built in the Swashbuckle NuGet package.
Special thanks to:
- My colleague Mehdi for reviewing this post before publishing;
- The swagger.io team who creates swagger-ui — https://swagger.io/;
- The Swashbuckle team for making it a breeze to incorporate these tools in ASP.NET Core apps — https://github.com/domaindrivendev/Swashbuckle.AspNetCore; and
- Andrew Lock for creating a NuGet package that makes it so easy to set up security-related HTTP headers — https://github.com/andrewlock/NetEscapades.AspNetCore.SecurityHeaders.