diff options
author | Matthew Somerville <matthew@mysociety.org> | 2019-11-25 13:59:08 +0000 |
---|---|---|
committer | Matthew Somerville <matthew@mysociety.org> | 2019-11-25 13:59:08 +0000 |
commit | e64110f3ee50f6d8f4b3e04df7ed6cd6443c114f (patch) | |
tree | 473064952ce207e8c3852d6d1e953888d0498dc7 | |
parent | 3936729479271dc84edf01e0ff840125a61eeb84 (diff) | |
parent | a1b76bb7873c002a987132280395093d03992b13 (diff) |
Merge branch 'csp-uk'
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | conf/general.yml-example | 4 | ||||
-rw-r--r-- | docs/customising/config.md | 28 | ||||
-rw-r--r-- | perllib/FixMyStreet/App.pm | 2 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/Default.pm | 17 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/FixMyStreet.pm | 10 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/UK.pm | 5 | ||||
-rw-r--r-- | t/app/01app.t | 31 | ||||
-rw-r--r-- | t/cobrand/councils.t | 24 |
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> & <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(); |