From 9516f1614c2f19b4746380fd240e9e3396fa7aa7 Mon Sep 17 00:00:00 2001 From: Steve Breker Date: Thu, 17 Aug 2023 09:58:49 -0700 Subject: [PATCH] Add CSP Headers to AtoM Responses Add Content Security Policy (CSP) headers to AtoM responses when B5 themes are enabled. The 'app_csp_reponse_header' setting is used to switch between using 'Content-Security-Policy-Report-Only' or 'Content-Security-Policy' headers. Deleting the setting will disable CSP headers. The 'app_csp_directives' setting is used to tweak the actual header contents. --- apps/qubit/config/filters.yml | 3 + config/app.yml | 9 +++ docker/bootstrap.php | 3 + lib/filter/QubitCSPFilter.php | 80 +++++++++++++++++++ .../accession/templates/browseSuccess.php | 9 ++- .../templates/_childLevels.php | 12 ++- .../templates/_identifierOptions.php | 7 +- .../modules/sfIsadPlugin/templates/_event.php | 8 +- webpack.config.js | 2 +- 9 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 lib/filter/QubitCSPFilter.php diff --git a/apps/qubit/config/filters.yml b/apps/qubit/config/filters.yml index f32765bafc..a4a9bed81a 100644 --- a/apps/qubit/config/filters.yml +++ b/apps/qubit/config/filters.yml @@ -25,5 +25,8 @@ QubitLimitResults: QubitTransaction: class: QubitTransactionFilter +QubitCSP: + class: QubitCSP + cache: ~ execution: ~ diff --git a/config/app.yml b/config/app.yml index c2a8ae4c9f..c8a378b313 100644 --- a/config/app.yml +++ b/config/app.yml @@ -59,3 +59,12 @@ all: # Maximum allowed value for full-width treeview "items per page" setting treeview_items_per_page_max: 10000 + + # Content Security Policy (CSP) header configuration. CSP settings are used + # only when a B5 theme is in use, otherwise these settings will be ignored. + csp: + # Configure CSP response header to be either + # 'Content-Security-Policy-Report-Only' or 'Content-Security-Policy' + response_header: Content-Security-Policy-Report-Only + # Configure CSP response directives. + directives: default-src 'self'; font-src 'self'; img-src 'self' https://www.gravatar.com/avatar/; script-src 'self'; style-src 'self' 'nonce'; frame-ancestors 'self'; diff --git a/docker/bootstrap.php b/docker/bootstrap.php index be93ae442c..bd4cb8f58b 100644 --- a/docker/bootstrap.php +++ b/docker/bootstrap.php @@ -104,6 +104,9 @@ function get_host_and_port($value, $default_port) persistent: true read_only: false htmlpurifier_enabled: false + csp: + response_header: Content-Security-Policy-Report-Only + directives: default-src 'self'; font-src 'self'; img-src 'self' https://www.gravatar.com/avatar/; script-src 'self'; style-src 'self' 'nonce'; frame-ancestors 'self'; EOT; file_put_contents(_ATOM_DIR.'/apps/qubit/config/app.yml', $app_yml); diff --git a/lib/filter/QubitCSPFilter.php b/lib/filter/QubitCSPFilter.php new file mode 100644 index 0000000000..367f41c3fc --- /dev/null +++ b/lib/filter/QubitCSPFilter.php @@ -0,0 +1,80 @@ +. + */ + +class QubitCSP extends sfFilter +{ + public function execute($filterChain) + { + // Only use CSP if theme is b5. + if (sfConfig::get('app_b5_theme', false)) { + $cspResponseHeader = sfConfig::get('app_csp_response_header', ''); + + if (empty($cspResponseHeader)) { + // CSP is deactivated. + $filterChain->execute(); + + return; + } + + $context = $this->getContext(); + if (false === array_search($cspResponseHeader, ['Content-Security-Policy-Report-Only', 'Content-Security-Policy'])) { + $context->getLogger()->err( + sprintf( + 'Setting \'app_csp_response_header\' is not set properly. CSP is not being used.' + ) + ); + + $filterChain->execute(); + + return; + } + + $cspDirectives = sfConfig::get('app_csp_directives', ''); + if (empty($cspDirectives)) { + $context->getLogger()->err( + sprintf( + 'Setting \'app_csp_directives\' is not set properly. CSP is not being used.' + ) + ); + + $filterChain->execute(); + + return; + } + + $nonce = $this->getRandomNonce(); + // Set response header. + $context->response->setHttpHeader( + $cspResponseHeader, + $cspDirectives = str_replace('nonce', 'nonce-'.$nonce, $cspDirectives) + ); + // Save for use in templates. + sfConfig::set('csp_nonce', 'nonce='.$nonce); + } + + $filterChain->execute(); + } + + protected function getRandomNonce($length = 32) + { + $string = md5(rand()); + + return substr($string, 0, $length); + } +} diff --git a/plugins/arDominionB5Plugin/modules/accession/templates/browseSuccess.php b/plugins/arDominionB5Plugin/modules/accession/templates/browseSuccess.php index dc9020a631..fbed2acc6d 100644 --- a/plugins/arDominionB5Plugin/modules/accession/templates/browseSuccess.php +++ b/plugins/arDominionB5Plugin/modules/accession/templates/browseSuccess.php @@ -43,17 +43,20 @@ getResults() as $hit) { ?> getData(); ?> - + + 'accession', 'slug' => $doc['slug']]); ?> 'accession', 'slug' => $doc['slug']]); ?> - + sort) { ?> - + - + + - + - + - + diff --git a/plugins/arDominionB5Plugin/modules/informationobject/templates/_identifierOptions.php b/plugins/arDominionB5Plugin/modules/informationobject/templates/_identifierOptions.php index ac77d98392..93b988538a 100644 --- a/plugins/arDominionB5Plugin/modules/informationobject/templates/_identifierOptions.php +++ b/plugins/arDominionB5Plugin/modules/informationobject/templates/_identifierOptions.php @@ -47,10 +47,13 @@ class="collapse" - -
+ + + diff --git a/plugins/arDominionB5Plugin/modules/sfIsadPlugin/templates/_event.php b/plugins/arDominionB5Plugin/modules/sfIsadPlugin/templates/_event.php index 35eb13ca19..2361c3af76 100644 --- a/plugins/arDominionB5Plugin/modules/sfIsadPlugin/templates/_event.php +++ b/plugins/arDominionB5Plugin/modules/sfIsadPlugin/templates/_event.php @@ -7,10 +7,14 @@ - -
+ + + diff --git a/webpack.config.js b/webpack.config.js index 60d196a5b1..0f7bd799b8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -79,7 +79,7 @@ module.exports = { filename: "js/[name].bundle.[contenthash].js", clean: true, }, - devtool: devMode ? "eval-source-map" : "source-map", + devtool: "source-map", module: { rules: [ {