aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthew Somerville <matthew@mysociety.org>2019-11-25 13:59:08 +0000
committerMatthew Somerville <matthew@mysociety.org>2019-11-25 13:59:08 +0000
commite64110f3ee50f6d8f4b3e04df7ed6cd6443c114f (patch)
tree473064952ce207e8c3852d6d1e953888d0498dc7
parent3936729479271dc84edf01e0ff840125a61eeb84 (diff)
parenta1b76bb7873c002a987132280395093d03992b13 (diff)
Merge branch 'csp-uk'
-rw-r--r--CHANGELOG.md1
-rw-r--r--conf/general.yml-example4
-rw-r--r--docs/customising/config.md28
-rw-r--r--perllib/FixMyStreet/App.pm2
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm17
-rw-r--r--perllib/FixMyStreet/Cobrand/FixMyStreet.pm10
-rw-r--r--perllib/FixMyStreet/Cobrand/UK.pm5
-rw-r--r--t/app/01app.t31
-rw-r--r--t/cobrand/councils.t24
9 files changed, 109 insertions, 13 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03d6a83f0..387d90114 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -60,6 +60,7 @@
- Sass variables for default link colour and decoration.
- Make contact edit note optional on staging sites.
- Store email addresses report sent to on the report.
+ - Add configuration for setting Content-Security-Policy header.
- Open311 improvements:
- Support use of 'private' service definition <keywords> to mark
reports made in that category private. #2488
diff --git a/conf/general.yml-example b/conf/general.yml-example
index 71e994dc0..86da4cf2c 100644
--- a/conf/general.yml-example
+++ b/conf/general.yml-example
@@ -28,6 +28,10 @@ SECURE_PROXY_SSL_HEADER: ''
# If you're behind a proxy, set this to a two-element list containing the
# trusted HTTP header and the required value. For example:
# SECURE_PROXY_SSL_HEADER: [ 'X-Forwarded-Proto', 'https' ]
+CONTENT_SECURITY_POLICY: ''
+# To activate CSP, set this to 1. If you have additional third party domains to
+# allow JavaScript on, or want to specify extra script-src CSP directives, set
+# this to a e.g. space-separated list of domains or a list of the same.
# Email domain used for emails, and contact name/email for admin use.
EMAIL_DOMAIN: 'example.org'
diff --git a/docs/customising/config.md b/docs/customising/config.md
index d83e00472..f3a023b1b 100644
--- a/docs/customising/config.md
+++ b/docs/customising/config.md
@@ -56,6 +56,7 @@ The following are all the configuration settings that you can change in `conf/ge
* <code><a href="#base_url">BASE_URL</a></code>
* <code><a href="#secure_proxy_ssl_header">SECURE_PROXY_SSL_HEADER</a></code>
+* <code><a href="#content_security_policy">CONTENT_SECURITY_POLICY</a></code>
* <code><a href="#geo_cache">GEO_CACHE</a></code>
* <code><a href="#admin_base_url">ADMIN_BASE_URL</a></code>
@@ -202,6 +203,33 @@ The following are all the configuration settings that you can change in `conf/ge
</dd>
<dt>
+ <a name="content_security_policy"><code>CONTENT_SECURITY_POLICY</code></a>
+ </dt>
+ <dd>
+ A Content-Security-Policy header can prevent cross-site scripting,
+ clickjacking and other code injection attacks (see
+ <a href="https://en.wikipedia.org/wiki/Content_Security_Policy">Wikipedia</a>
+ for more). To have FixMyStreet output such a header, set this setting to 1.
+ If you load third-party JavaScript on your site, you will need to set this
+ setting to a space-separated list of domains; whatever is here, if not 1,
+ will be included in the header output.
+ <div class="more-info">
+ <p>Example:</p>
+ <ul class="examples">
+ <li>
+ <code>CONTENT_SECURITY_POLICY: 1</code>
+ </li>
+ <li>
+ <code>CONTENT_SECURITY_POLICY: 'www.example.org other.example.org'</code>
+ </li>
+ <li>
+ <code>CONTENT_SECURITY_POLICY: [ 'www.example.org', 'other.example.org' ]</code>
+ </li>
+ </ul>
+ </div>
+ </dd>
+
+ <dt>
<a name="email_domain"><code>EMAIL_DOMAIN</code></a>,
<a name="contact_email"><code>CONTACT_EMAIL</code></a> &amp;
<a name="contact_name"><code>CONTACT_NAME</code></a>
diff --git a/perllib/FixMyStreet/App.pm b/perllib/FixMyStreet/App.pm
index 42556d1df..f62deae3a 100644
--- a/perllib/FixMyStreet/App.pm
+++ b/perllib/FixMyStreet/App.pm
@@ -199,7 +199,7 @@ sub setup_request {
my $cobrand = $c->cobrand;
FixMyStreet::DB->schema->cobrand($cobrand);
- $cobrand->call_hook('add_response_headers');
+ $cobrand->add_response_headers;
# append the cobrand templates to the include path
$c->stash->{additional_template_paths} = $cobrand->path_to_web_templates;
diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm
index 620183078..1f2e48994 100644
--- a/perllib/FixMyStreet/Cobrand/Default.pm
+++ b/perllib/FixMyStreet/Cobrand/Default.pm
@@ -14,6 +14,7 @@ use Digest::MD5 qw(md5_hex);
use Carp;
use mySociety::PostcodeUtil;
+use mySociety::Random;
=head1 The default cobrand
@@ -74,6 +75,22 @@ sub feature {
return $features->{$feature}->{$self->moniker};
}
+sub csp_config {
+ FixMyStreet->config('CONTENT_SECURITY_POLICY');
+}
+
+sub add_response_headers {
+ my $self = shift;
+ # uncoverable branch true
+ return if $self->{c}->debug;
+ if (my $csp_domains = $self->csp_config) {
+ $csp_domains = '' if $csp_domains eq '1';
+ $csp_domains = join(' ', @$csp_domains) if ref $csp_domains;
+ my $csp_nonce = $self->{c}->stash->{csp_nonce} = unpack('h*', mySociety::Random::random_bytes(16, 1));
+ $self->{c}->res->header('Content-Security-Policy', "script-src 'self' 'unsafe-inline' 'nonce-$csp_nonce' $csp_domains; object-src 'none'; base-uri 'none'")
+ }
+}
+
=item password_minimum_length
Returns the minimum length a password can be set to.
diff --git a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
index 8ef51f328..0d2bf3663 100644
--- a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
+++ b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
@@ -4,8 +4,6 @@ use base 'FixMyStreet::Cobrand::UK';
use strict;
use warnings;
-use mySociety::Random;
-
use constant COUNCIL_ID_BROMLEY => 2482;
use constant COUNCIL_ID_ISLEOFWIGHT => 2636;
@@ -25,14 +23,6 @@ sub path_to_email_templates {
];
}
-sub add_response_headers {
- my $self = shift;
- # uncoverable branch true
- return if $self->{c}->debug;
- my $csp_nonce = $self->{c}->stash->{csp_nonce} = unpack('h*', mySociety::Random::random_bytes(16, 1));
- $self->{c}->res->header('Content-Security-Policy', "script-src 'self' www.google-analytics.com www.googleadservices.com 'unsafe-inline' 'nonce-$csp_nonce'")
-}
-
# FixMyStreet should return all cobrands
sub restriction {
return {};
diff --git a/perllib/FixMyStreet/Cobrand/UK.pm b/perllib/FixMyStreet/Cobrand/UK.pm
index 18bf01476..83df590db 100644
--- a/perllib/FixMyStreet/Cobrand/UK.pm
+++ b/perllib/FixMyStreet/Cobrand/UK.pm
@@ -11,6 +11,11 @@ sub country { return 'GB'; }
sub area_types { [ 'DIS', 'LBO', 'MTD', 'UTA', 'CTY', 'COI', 'LGD' ] }
sub area_types_children { $mySociety::VotingArea::council_child_types }
+sub csp_config {
+ my $self = shift;
+ return $self->feature('content_security_policy');
+}
+
sub enter_postcode_text {
my ( $self ) = @_;
return _("Enter a nearby UK postcode, or street name and area");
diff --git a/t/app/01app.t b/t/app/01app.t
index 7b933973b..50617d491 100644
--- a/t/app/01app.t
+++ b/t/app/01app.t
@@ -17,7 +17,6 @@ use charnames ':full';
ok( request('/')->is_success, 'Request should succeed' );
-SKIP: {
FixMyStreet::override_config {
ALLOWED_COBRANDS => [ 'tester' ],
}, sub {
@@ -25,6 +24,34 @@ FixMyStreet::override_config {
my $num = "12( | )345";
like $page, qr/$num/;
};
-}
+
+subtest 'CSP header' => sub {
+ my $res = request('/');
+ is $res->header('Content-Security-Policy'), undef, 'None by default';
+
+ FixMyStreet::override_config {
+ CONTENT_SECURITY_POLICY => 1,
+ }, sub {
+ my $res = request('/');
+ like $res->header('Content-Security-Policy'), qr/script-src 'self' 'unsafe-inline' 'nonce-[^']*' ; object-src 'none'; base-uri 'none'/,
+ 'Default CSP header if requested';
+ };
+
+ FixMyStreet::override_config {
+ CONTENT_SECURITY_POLICY => 'www.example.org',
+ }, sub {
+ my $res = request('/');
+ like $res->header('Content-Security-Policy'), qr/script-src 'self' 'unsafe-inline' 'nonce-[^']*' www.example.org; object-src 'none'; base-uri 'none'/,
+ 'With 3P domains if given';
+ };
+
+ FixMyStreet::override_config {
+ CONTENT_SECURITY_POLICY => [ 'www.example.org' ],
+ }, sub {
+ my $res = request('/');
+ like $res->header('Content-Security-Policy'), qr/script-src 'self' 'unsafe-inline' 'nonce-[^']*' www.example.org; object-src 'none'; base-uri 'none'/,
+ 'With 3P domains if given';
+ };
+};
done_testing();
diff --git a/t/cobrand/councils.t b/t/cobrand/councils.t
index a194a9be1..aac682b19 100644
--- a/t/cobrand/councils.t
+++ b/t/cobrand/councils.t
@@ -90,5 +90,29 @@ subtest "Test update shown/not shown appropriately" => sub {
}
};
+subtest "CSP header from feature" => sub {
+ foreach my $cobrand (
+ { moniker => 'oxfordshire', test => 'oxon.analytics.example.org' },
+ { moniker =>'fixmystreet', test => '' },
+ { moniker => 'nonsecure', test => undef },
+ ) {
+ FixMyStreet::override_config {
+ ALLOWED_COBRANDS => $cobrand->{moniker},
+ COBRAND_FEATURES => {
+ content_security_policy => {
+ oxfordshire => 'oxon.analytics.example.org',
+ fixmystreet => 1,
+ }
+ },
+ }, sub {
+ $mech->get_ok("/");
+ if (defined $cobrand->{test}) {
+ like $mech->res->header('Content-Security-Policy'), qr/script-src 'self' 'unsafe-inline' 'nonce-[^']*' $cobrand->{test}/;
+ } else {
+ is $mech->res->header('Content-Security-Policy'), undef;
+ }
+ };
+ }
+};
done_testing();