diff options
author | Matthew Somerville <matthew@mysociety.org> | 2019-11-22 08:24:07 +0000 |
---|---|---|
committer | Matthew Somerville <matthew@mysociety.org> | 2019-11-25 12:25:59 +0000 |
commit | f0d7a3babca129a8ffd6d7aa4de9aaa74df475ed (patch) | |
tree | 37e622e6d9efc9616d20e83398847f28b9db2671 | |
parent | 399a38c4636fac6ce4a2eb21053604ba74309a36 (diff) |
Add configuration for setting CSP header.
This allows you to output a working Content-Security-Policy header, with
optional third-party domains, by setting a new CONTENT_SECURITY_POLICY
configuration option.
-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-- | t/app/01app.t | 31 |
6 files changed, 80 insertions, 3 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index f85991373..48cc45027 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,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 1173523bc..31bf3e400 100644 --- a/perllib/FixMyStreet/App.pm +++ b/perllib/FixMyStreet/App.pm @@ -198,7 +198,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/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(); |