Anthony Chu Contact Me

Implementing Content Security Policy (CSP) in ASP.NET Core

Sunday, March 13, 2016

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. These attacks are used for everything from data theft to site defacement or distribution of malware.

-- MDN article on CSP

In this post we'll add CSP to an ASP.NET Core app.

Introduction to CSP

Content Security Policy, in a nutshell, is a way for a web page to control what resources are allowed to be loaded. For example, a page can explicitly declare domains that JavaScript, CSS, and image resources (and more!) are permitted to be loaded from. This can help prevent things like cross-site scripting (XSS) attacks.

It can be used to restrict protocols as well, such as restricting content to be loaded via HTTPS.

CSP is implemented via a Content-Security-Policy header in an HTTP response.

Adding CSP Response Headers

A quick-and-dirty way to add CSP headers to responses for an entire ASP.NET Core app is to use middleware. Here's a simple middleware that we can add to our Configure() method in Startup.cs (it needs to be ahead of MVC in the pipeline):

app.Use(async (ctx, next) => 
{
    ctx.Response.Headers.Add("Content-Security-Policy", 
                             "default-src 'self'; report-uri /cspreport");
    await next();
});

In this example, we're not getting too fancy with CSP; we'll simply instruct the browser to only allow resources to be loaded from the domain the page is served from. Here's what happens when we try to load Bootstrap's CSS file from a CDN:

CSP Report

The browser will report any CSP violations to a /cspreport endpoint in our application. We'll build this endpoint next.

Creating the CSP Report Endpoint

CSP Report Request Objects

If we inspect the network request that the browser makes to the reporting endpoint, we'll see a payload like this:

{
    "csp-report": {
        "document-uri": "http://localhost:5000/",
        "referrer": "",
        "violated-directive": "default-src 'self'",
        "effective-directive": "style-src",
        "original-policy": "default-src 'self'; report-uri /cspreport",
        "blocked-uri": "https://ajax.aspnetcdn.com",
        "status-code": 200
    }
}

We can easily create a couple of objects to represent that in .NET. Unfortunately the property names have dashes, so we'll need to use the JsonPropertyAttribute to tell JSON.NET how they map to the .NET object's properties:

public class CspReportRequest
{
    [JsonProperty(PropertyName = "csp-report")]
    public CspReport CspReport { get; set; }
}

public class CspReport
{
    [JsonProperty(PropertyName = "document-uri")]
    public string DocumentUri { get; set; }

    [JsonProperty(PropertyName = "referrer")]
    public string Referrer { get; set; }

    [JsonProperty(PropertyName = "violated-directive")]
    public string ViolatedDirective { get; set; }

    [JsonProperty(PropertyName = "effective-directive")]
    public string EffectiveDirective { get; set; }

    [JsonProperty(PropertyName = "original-policy")]
    public string OriginalPolicy { get; set; }

    [JsonProperty(PropertyName = "blocked-uri")]
    public string BlockedUri { get; set; }

    [JsonProperty(PropertyName = "status-code")]
    public int StatusCode { get; set; }
}

CSP Report Endpoint

The endpoint that the browser will post the CSP violations to is a simple Web API action on our MVC controller:

[HttpPost("~/cspreport")]
public IActionResult CspReport([FromBody] CspReportRequest request)
{
    // TODO: log request to a datastore somewhere
    _logger.LogWarning($"CSP Violation: {request.CspReport.DocumentUri}, {request.CspReport.BlockedUri}");

    return Ok();
}

In real life, we'll want to do something more useful than just logging a message to the console. We can write them to a database, an Azure service (SQL DB, Table Storage, Event Hubs, and Application Insights come to mind), or an error logging/reporting service like Sentry, Airbrake, or Raygun.

Custom Media Types in JsonInputFormatter

ASP.NET MVC's model-binding uses content negotiation to determine the format of an incoming POST request. Because browsers will post CSP reports with a Content-Type header value of application/csp-report, we'll have to tell the JsonInputFormatter to respond to requests with this media type.

To do this, we have to replace the JsonInputFormatter already registered with MVC with one that has an additional item in SupportedMediaTypes for application/csp-report. We do this in ConfigureServices():

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.Configure<MvcOptions>(options =>
    {
        var formatter = new JsonInputFormatter();
        formatter.SupportedMediaTypes
            .Add(MediaTypeHeaderValue.Parse("application/csp-report"));
        options.InputFormatters.RemoveType<JsonInputFormatter>();
        options.InputFormatters.Add(formatter);
    });
}

Now when we hit the page again, we should see the log message appear in the console or debug window:

warn: csp_report.Controllers.HomeController[0]
      CSP Violation: http://localhost:5000/, https://ajax.aspnetcdn.com

Source Code

A full working sample can be found at: https://github.com/anthonychu/aspnet-core-csp

Thanks to Troy Hunt for the tweet that gave me the inspiration for this post.

Update - For a service that captures and analyzes CSP violations, check out report-uri.io.