aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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--t/app/01app.t31
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> &amp;
<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();