Skip to content

Commit

Permalink
Add CSP Headers to AtoM Responses
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sbreker committed Aug 23, 2023
1 parent 9dd0656 commit 146f817
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 12 deletions.
3 changes: 3 additions & 0 deletions apps/qubit/config/filters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@ QubitLimitResults:
QubitTransaction:
class: QubitTransactionFilter

QubitCSP:
class: QubitCSP

cache: ~
execution: ~
9 changes: 9 additions & 0 deletions config/app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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';
3 changes: 3 additions & 0 deletions docker/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
80 changes: 80 additions & 0 deletions lib/filter/QubitCSPFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

/*
* This file is part of the Access to Memory (AtoM) software.
*
* Access to Memory (AtoM) is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Access to Memory (AtoM) is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Access to Memory (AtoM). If not, see <http://www.gnu.org/licenses/>.
*/

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,20 @@
<?php foreach ($pager->getResults() as $hit) { ?>
<?php $doc = $hit->getData(); ?>
<tr>
<td width="20%">
<style <?php echo __(sfConfig::get('csp_nonce', '')); ?>>
#browse-results-identifier, #browse-results-date, #browse-results-updated-at { width: 20% }
</style>
<td id="browse-results-identifier">
<?php echo link_to($doc['identifier'], ['module' => 'accession', 'slug' => $doc['slug']]); ?>
</td>
<td>
<?php echo link_to(render_title(get_search_i18n($doc, 'title')), ['module' => 'accession', 'slug' => $doc['slug']]); ?>
</td>
<td width="20%">
<td id="browse-results-date">
<?php echo format_date($doc['date'], 'i'); ?>
</td>
<?php if ('lastUpdated' == $sf_request->sort) { ?>
<td width="20%">
<td id="browse-results-updated-at>
<?php echo format_date($doc['updatedAt'], 'f'); ?>
</td>
<?php } ?>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@
<table class="table table-bordered mb-0 multi-row">
<thead class="table-light">
<tr>
<th id="child-identifier-head" style="width: 20%">
<style <?php echo __(sfConfig::get('csp_nonce', '')); ?>>
#child-identifier-head, #child-level-head, #child-date-head { width: 20% }
#child-title-head { width: 40% }
</style>
<th id="child-identifier-head">
<?php echo __('Identifier'); ?>
</th>
<th id="child-level-head" style="width: 20%">
<th id="child-level-head">
<?php echo __('Level'); ?>
</th>
<th id="child-title-head" style="width: 40%">
<th id="child-title-head">
<?php echo __('Title'); ?>
</th>
<th id="child-date-head" style="width: 20%">
<th id="child-date-head">
<?php echo __('Date'); ?>
</th>
<th>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,13 @@ class="collapse<?php echo 0 < count($alternativeIdentifiers) ? ' show' : ''; ?>"
<table class="table table-bordered mb-0 multi-row">
<thead class="table-light">
<tr>
<th id="alt-identifiers-label-head" style="width: 50%">
<style <?php echo __(sfConfig::get('csp_nonce', '')); ?>>
#alt-identifiers-label-head, #alt-identifiers-identifier-head { width: 50% }
</style>
<th id="alt-identifiers-label-head">
<?php echo __('Label'); ?>
</th>
<th id="alt-identifiers-identifier-head" style="width: 50%">
<th id="alt-identifiers-identifier-head">
<?php echo __('Identifier'); ?>
</th>
<th>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
<table class="table table-bordered mb-0 multi-row">
<thead class="table-light">
<tr>
<th id="isad-events-type-head" style="width: 25%">
<style <?php echo __(sfConfig::get('csp_nonce', '')); ?>>
#isad-events-type-head { width: 25% }
#isad-events-date-head { width: 30% }
</style>
<th id="isad-events-type-head">
<?php echo __('Type'); ?>
</th>
<th id="isad-events-date-head" style="width: 30%">
<th id="isad-events-date-head">
<?php echo __('Date'); ?>
</th>
<th id="isad-events-start-head">
Expand Down
2 changes: 1 addition & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ module.exports = {
filename: "js/[name].bundle.[contenthash].js",
clean: true,
},
devtool: devMode ? "eval-source-map" : "source-map",
devtool: devMode ? "source-map" : "source-map",
module: {
rules: [
{
Expand Down

0 comments on commit 146f817

Please sign in to comment.