DIY Security Reports with Umbraco and NWebSec

Posted by Phil on February 27, 2018

I've written before about NWebSec and how easy it makes building out your own website's security policies. Since implementing the policy for this site, I've noticed I don't quite have things set 100% correctly and I was also conscious that I'm not actually checking regularly to see what sort of things might be causing me further trouble downstream.

In other words, I'm not reporting on my Content Security Policy (CSP).

Content Security Policies

To recap, a CSP is an HTTP response header that allows website administrators to control the resources the user agent (i.e. browser) is allowed to load for a given page. It mainly dictates what scripts and originating servers are ok for the browser to load.

There are a bunch of different resource directives included as part of the CSP spec, but there's also a really useful one that controls the reporting process of CSP violations. That is, "what should I do if there's something requested from the site that has been disallowed"? In those cases, you can define a ruleset for the browser to notify you that something fishy may be going on.

In our NWebSec configuration within the Web.Config file, we can set this very easily simply by adding the following node within the <content-Security-Policy> node:

          <report-uri enableBuiltinHandler="true" enabled="true">
            ...
          </report-uri>

That's about all there is to it. The "report-uri" section (when enabled) instructs the browser to report attempts to violate the CSP to the URLs added within. So in this case, we can define an endpoint and add this to our report-uri node:

 <report-uri enableBuiltinHandler="true" enabled="true">
<add report-uri="/umbraco/api/urireports/csp"/>
</report-uri>

This defines a web service endpoint that the browser will POST to, containing a JSON document with all the relevant information.

I've defined a new Umbraco API controller here that has a single endpoint, "csp". I want that endpoint to accept only an HTTPPost request and return a status code indicating whether that post was successful or not. I don't want it to prevent my application from running so if it's not successful, I'm taking a pretty light approach.

Before I start this however, I need to tell Umbraco to do a couple of things: the user agent will POST the JSON document with a specific content type header: "Content-Type: application/csp-report". While this is still just ordinary JSON, the standard set of HTTP document media type formatters does not recognise this specific type and will throw a "415 - Unsupported Media Type" exception in my API method. I need Umbraco to recognise that media type.

The other thing I want to do is have Umbraco save my reports in a custom table within the database itself. I could create a collection of content documents and have those saved to my main Umbraco document tree, but this isn't really the sort of thing I want cluttering up my site content with. These are more under a "site logs" type of category. Umbraco can write Plain Old Class Objects directly to the database in much the same way as .Net developers have become used to Entity Framework Code First.

I can define a table name and model type that I want to initialise and use to store my reports. To do this, I'm going to hook into Umbraco's application event handler and define custom code for the ApplicationStarted event:

    public class RegisterEvents : ApplicationEventHandler
    {
        protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
            MediaTypeHeaderValue cspHeaderValue = new MediaTypeHeaderValue("application/csp-report");
          
            GlobalConfiguration.Configuration.Formatters.JsonFormatter.SupportedMediaTypes.Add(cspHeaderValue);

            DatabaseContext ctx = ApplicationContext.Current.DatabaseContext;
            DatabaseSchemaHelper dbSchema = new DatabaseSchemaHelper(ctx.Database, ApplicationContext.Current.ProfilingLogger.Logger, ctx.SqlSyntax);

            if (!dbSchema.TableExist("cspReports")) dbSchema.CreateTable<CspReportModel>(false);


            base.ApplicationStarted(umbracoApplication, applicationContext);
        }

    }

So here I've achieved two things: I define a new media type for Umbraco to recognise as a valid HTTP header, and I set up the application's database context to create a "cspReports" table (if it doesn't exist already) and initialise that table according to the attributes of my "CspReportModel" class.

Here's how my CspReportModel class looks:

using System;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.DatabaseAnnotations;

namespace MyUmbracoSite.Models
{
    [TableName("cspReports")]
    [PrimaryKey("Id", autoIncrement = true)]
    public class CspReportModel
    {
        [Column("Id")]
        [PrimaryKeyColumn(AutoIncrement = true)]
        public int Id { get; set; }

        [Column("RecordedOn")]
        public DateTime RecordedOn { get; set; }
        [Length(200)]
        public string BlockedUri { get; set; }
        [Length(200)]
        public string DocumentUri { get; set; }
        public int Line { get; set; }
        [Length(800)]
        public string Policy { get; set; }
        [Length(800)]
        public string ViolatedDirective { get; set; }
    }
}

Again, the table name is defined and I've used data annotations to set the column names if I want them to differ from my actual data model (basically for demonstration purposes here). I've also ensured I've expressly defined the field lengths because if a string POCO object is created without a length, the default length is 1. That's going to cause problems if we're storing large text values.

So that's our groundwork done. The only remaining task is to create the actual API.

    public class UriReportsController : UmbracoApiController
    {
        [HttpPost]
        public HttpStatusCodeResult Csp(JObject report)
        {
            try
            {
                var cspReport = new CspReportModel
                {
                    BlockedUri = report.SelectToken("$.csp-report.blocked-uri").ToString(),
                    DocumentUri = report.SelectToken("$.csp-report.document-uri").ToString(),
                    RecordedOn = DateTime.Now,
                    //Line = int.Parse(report.SelectToken("$.csp-report.line-number").ToString()),
                    Policy = report.SelectToken("$.csp-report.original-policy").ToString(),
                    ViolatedDirective = report.SelectToken("$.csp-report.violated-directive").ToString()
                };

                SaveReport(cspReport);

                return new HttpStatusCodeResult(202);
            }
            catch (Exception ex)
            {
                // Do something with the error as well
                return new HttpStatusCodeResult(HttpStatusCode.ExpectationFailed);
            }

        }

        private void SaveReport(CspReportModel report)
        {
            DatabaseContext.Database.Save(report);
        }
    }

Here I've created an Umbraco API Controller and have added a single "Csp" method accepting only a POST. The argument is a JObject mainly because I'm taking a lazy approach and instead of trying to deserialize the parameter directly to a strongly-typed object, I'm just picking out the bits of the JSON document that I'm interested in right now. I'm sure there are more elegant ways to handle this.

From there, I picked out values I need using Json.Net's path queries, pass those into a new CspReportModel class and save the result directly to the database.

Job done. I now have a table that will collect security violation information on my site.

In an upcoming post I'll look at how to present the information gathered.

 

Report-Uri.com

It should be pointed out that Scott Helme has already solved this problem with his very excellent service, https://report-uri.com. This service makes reporting CSP (and many other) security alerts incredibly easy and provides charts and metrics that make understanding your site's behaviour simple. Scott offers a free tier for the service as well.

It's an even easier task to have our CSP report directly to report-uri.com by adding that reference into our Web.Config:

  <report-uri enableBuiltinHandler="true" enabled="true">
    <add report-uri="https://[yoursitename].report-uri.com/r/d/csp/enforce"/>
    <add report-uri="/umbraco/api/urireports/csp"/>
  </report-uri>

 

Just sign up to the service, define your subdomain (e.g. [yoursitename] in the example above) and copy the URL provided for reporting CSP violations.

 

Photo Credits

unsplash-logoTim Evans

unsplash-logobady qb