How to lock down your CSP when using Swashbuckle

Introduction

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:

  1. Allowing the hashes of the contents of the inline <script> and <style> tags (see https://scotthelme.co.uk/csp-cheat-sheet/#hashes); or
  2. 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

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:

  1. It contains an inline <script> tag; and
  2. 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:

  1. We need to maintain a modified copy of a dependency file;
  2. 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:

  1. 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;
  2. 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;
  3. 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 the DOMContentLoaded 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!

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: