Skip to content

Commit

Permalink
Merge pull request #402 from PGScatalog/improve/implement_csp
Browse files Browse the repository at this point in the history
Replacing X-Frame-Options HTTP header with CSP
  • Loading branch information
fyvon authored Jan 17, 2025
2 parents 0a29dac + 311fd74 commit a951d15
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 30 deletions.
16 changes: 16 additions & 0 deletions catalog/context_processors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django.conf import settings
from pgs_web import constants
from pgs_web import external_urls


def pgs_settings(request):
return {
Expand All @@ -8,6 +10,7 @@ def pgs_settings(request):
'is_pgs_curation_site': settings.PGS_ON_CURATION_SITE
}


def pgs_urls(request):
return {
'nhgri_url' : constants.USEFUL_URLS['NHGRI_URL'],
Expand All @@ -28,6 +31,17 @@ def pgs_urls(request):
'catalog_publication_url': constants.USEFUL_URLS['CATALOG_PUBLICATION_URL']
}


def styles_urls(request):
return {
'bootstrap_style_url': external_urls.STYLES_URLS['bootstrap'],
'bootstrap_table_style_url': external_urls.STYLES_URLS['bootstrap-table'],
'font_awesome_style_url': external_urls.STYLES_URLS['font-awesome'],
'jquery_style_url': external_urls.STYLES_URLS['jquery'],
'ebi_style_url': external_urls.STYLES_URLS['ebi'],
}


def pgs_search_examples(request):
eg_count = 0
html = ''
Expand All @@ -46,6 +60,7 @@ def pgs_search_examples(request):
'pgs_search_examples': html
}


def pgs_info(request):
return {
'pgs_citation': constants.PGS_CITATIONS[0],
Expand All @@ -55,6 +70,7 @@ def pgs_info(request):
'ensembl_version': constants.ENSEMBL_VERSION
}


def pgs_contributors(request):
groups = constants.PGS_GROUPS
groups_to_print = list()
Expand Down
23 changes: 23 additions & 0 deletions catalog/middleware/add_nonce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import re


class AddNonceToScriptsMiddleware:
"""This middleware class adds the CSP nonce to every <script> html element if not already present."""
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
response = self.get_response(request)
if 'text/html' in response.get('Content-Type', ''):
# Getting the nonce random value generated by django-csp middleware
nonce = getattr(request, 'csp_nonce', '')
if nonce:
script_tag_pattern = r'<script(| (?![^>]* nonce=)([^>]*))>' # No need to sub if nonce already there
replacement = rf'<script\1 nonce="{nonce}">'
response.content = re.sub(
script_tag_pattern,
replacement,
response.content.decode('utf-8'),
flags=re.IGNORECASE
).encode('utf-8')
return response
10 changes: 5 additions & 5 deletions catalog/templates/catalog/libs/css.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@
{% endif %}

<!-- Boostrap CSS libraries -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v6.7.2/css/all.css" crossorigin="anonymous"/>
<link rel="stylesheet" href="{{ bootstrap_style_url }}" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<link rel="stylesheet" href="{{ font_awesome_style_url }}" crossorigin="anonymous"/>
{% if has_table %}
<link href="https://unpkg.com/bootstrap-table@1.21.3/dist/bootstrap-table.min.css" rel="stylesheet"/>
<link href="{{ bootstrap_table_style_url }}" rel="stylesheet"/>
{% endif %}

<!-- jQuery UI -->
<link rel="stylesheet" href="//code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css"/>
<link rel="stylesheet" href="{{ jquery_style_url }}"/>

<!-- CSS: implied media=all -->
<!-- CSS concatenated and minified via ant build script -->
{% if has_ebi_icons %}
<link rel="stylesheet" href="https://ebi.emblstatic.net/web_guidelines/EBI-Icon-fonts/v1.3/fonts.css" type="text/css" media="all">
<link rel="stylesheet" href="{{ ebi_style_url }}" type="text/css" media="all">
{% endif %}

<!-- PGS (S)CSS file -->
Expand Down
40 changes: 20 additions & 20 deletions catalog/templates/catalog/libs/js.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,62 @@
{% endif %}

<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous" nonce="{{request.csp_nonce}}"></script>
<!-- jQuery UI -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js" nonce="{{request.csp_nonce}}"></script>


<!-- Use of GWAS API - Javascript file -->
{% if use_gwas_api %}
<script defer src="{% static 'catalog/pgs_gwas.js' %}"></script>
<script defer src="{% static 'catalog/pgs_gwas.js' %}" nonce="{{request.csp_nonce}}"></script>
{% endif %}

<!-- Use of Release JS - Javascript file -->
{% if use_release_charts %}
<script defer src="{% static 'catalog/pgs_bar_charts.js' %}"></script>
<script defer src="{% static 'catalog/pgs_bar_charts.js' %}" nonce="{{request.csp_nonce}}"></script>
{% endif %}

<!-- Bootstrap bundle include Popper.js and Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous" nonce="{{request.csp_nonce}}"></script>
{% if has_table %}
<script defer src="https://unpkg.com/tableexport.jquery.plugin@1.26.0/tableExport.min.js"></script>
<script defer src="https://unpkg.com/bootstrap-table@1.21.3/dist/bootstrap-table.min.js"></script>
<script defer src="https://unpkg.com/bootstrap-table@1.21.3/dist/extensions/export/bootstrap-table-export.min.js"></script>
<script defer src="https://unpkg.com/bootstrap-table@1.21.3/dist/extensions/filter-control/bootstrap-table-filter-control.min.js"></script>
<script defer src="https://unpkg.com/tableexport.jquery.plugin@1.26.0/tableExport.min.js" nonce="{{request.csp_nonce}}"></script>
<script defer src="https://unpkg.com/bootstrap-table@1.21.3/dist/bootstrap-table.min.js" nonce="{{request.csp_nonce}}"></script>
<script defer src="https://unpkg.com/bootstrap-table@1.21.3/dist/extensions/export/bootstrap-table-export.min.js" nonce="{{request.csp_nonce}}"></script>
<script defer src="https://unpkg.com/bootstrap-table@1.21.3/dist/extensions/filter-control/bootstrap-table-filter-control.min.js" nonce="{{request.csp_nonce}}"></script>
{% endif %}

<!-- D3 graph library -->
{% if has_chart %}
<script src="https://unpkg.com/d3@7.6.1/dist/d3.min.js"></script>
<script src="https://unpkg.com/d3@7.6.1/dist/d3.min.js" nonce="{{request.csp_nonce}}"></script>
{% endif %}

<!-- Chart.js -->
{% if has_chart_js %}
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.0.0/chartjs-plugin-datalabels.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-adapter-moment/1.0.0/chartjs-adapter-moment.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js" nonce="{{request.csp_nonce}}"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.min.js" nonce="{{request.csp_nonce}}"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.0.0/chartjs-plugin-datalabels.min.js" nonce="{{request.csp_nonce}}"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-adapter-moment/1.0.0/chartjs-adapter-moment.min.js" nonce="{{request.csp_nonce}}"></script>
{% endif %}

<!-- PGS Javascript file -->
{% if is_pgs_app_on_gae %}
<script src="{% static 'catalog/pgs.min.js' %}"></script>
<script src="{% static 'catalog/pgs.min.js' %}" nonce="{{request.csp_nonce}}"></script>
{% else %}
{% compress js file pgs_min %}
<script src="{% static 'catalog/pgs.js' %}"></script>
<script src="{% static 'catalog/pgs.js' %}" nonce="{{request.csp_nonce}}"></script>
{% endcompress %}
{% endif %}


{% if is_benchmark and has_chart %}
<!-- Benchmark -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.min.js"></script>
<script src="https://cdn.rawgit.com/eligrey/FileSaver.js/e9d941381475b5df8b7d7691013401e171014e89/FileSaver.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.min.js" nonce="{{request.csp_nonce}}"></script>
<script src="https://cdn.rawgit.com/eligrey/FileSaver.js/e9d941381475b5df8b7d7691013401e171014e89/FileSaver.min.js" nonce="{{request.csp_nonce}}"></script>
{% if is_pgs_app_on_gae %}
<script defer src="{% static 'benchmark/benchmark.min.js' %}"></script>
<script defer src="{% static 'benchmark/benchmark.min.js' %}" nonce="{{request.csp_nonce}}"></script>
{% else %}
{% compress js file benchmark_min %}
<script defer src="{% static 'benchmark/benchmark.js' %}"></script>
<script defer src="{% static 'benchmark/benchmark.js' %}" nonce="{{request.csp_nonce}}"></script>
{% endcompress %}
{% endif %}
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ function setError(error){
}
}

function goToPGP(new_window){
function goToPGP(event){
var new_window = event.data.new_window;
setError('');
var pgp_id = $('#id_pgp_id').val();
if(pgp_id){
Expand All @@ -22,6 +23,7 @@ function goToPGP(new_window){
} else {
alert('Please provide a PGP ID first');
}
return false; // To avoid reloading the current page
}

function goToPublication(){
Expand All @@ -35,6 +37,7 @@ function goToPublication(){
} else {
alert('Please provide a DOI or PubMed ID');
}
return false; // To avoid reloading the current page
}

function toggleAuthorSub(){
Expand Down Expand Up @@ -113,6 +116,7 @@ function autofillForm(){
var doi = $('#id_doi').val();
var pmid = $('#id_PMID').val();
_getPublicationInfo({doi: doi, pmid: pmid});
return false; // To avoid reloading the current page
}

function requestAuthorData(){
Expand Down Expand Up @@ -141,10 +145,17 @@ $(document).ready(function(){
var pgp_id_div = $('div.form-row.field-pgp_id > div');
pgp_id_div.addClass('flex-container');
pgp_id_div.find(">:first-child").addClass('fieldBox');
pgp_id_div.append('<div><a title="Go to the PGS Catalog curation publication page" href="" class="extra-field-button external-link" onclick="goToPGP(false); return false;" onauxclick="goToPGP(true); return false;">Go to PGS Catalog publication</a></div>');
pgp_id_div.append('<div><a id="go_to_pgp_link" title="Go to the PGS Catalog curation publication page" href="" class="extra-field-button external-link">Go to PGS Catalog publication</a></div>');

// Adding 'go to publication' and 'Autofill' buttons after the DOI and PMID form fields
$('div.form-row.field-doi.field-PMID > div.flex-container').append('<div><div><a title="Go to the publication page using DOI or the Pubmed page if only the PMID is provided" href="" class="extra-field-button external-link" onclick="goToPublication(); return false;">Go to publication</a></div><div style="display: flex;"><div><a title="Fetch the publication data from EPMC and fill in the form automatically (DOI or PMID required)" href="" class="extra-field-button" onclick="autofillForm(); return false;">Autofill <i class="fa-solid fa-gears"></i></a></div><div id="doi_pmid_error" class="fieldBox errors"><ul class="errorlist"></ul></div></div></div>');
$('div.form-row.field-doi.field-PMID > div.flex-container').append('<div><div><a id="go_to_publication_link" title="Go to the publication page using DOI or the Pubmed page if only the PMID is provided" href="" class="extra-field-button external-link">Go to publication</a></div><div style="display: flex;"><div><a id="autofill_link" title="Fetch the publication data from EPMC and fill in the form automatically (DOI or PMID required)" href="" class="extra-field-button">Autofill <i class="fa-solid fa-gears"></i></a></div><div id="doi_pmid_error" class="fieldBox errors"><ul class="errorlist"></ul></div></div></div>');

// Adding actions to the buttons
var pgp_link_selector = $('#go_to_pgp_link')
pgp_link_selector.click({'new_window': false}, goToPGP);
pgp_link_selector.on('auxclick', {'new_window': true}, goToPGP);
$('#go_to_publication_link').click(goToPublication);
$('#autofill_link').click(autofillForm);

// Adding toggle AuthorSub suffix function
$('#id_author_submission').click(toggleAuthorSub);
Expand Down
7 changes: 7 additions & 0 deletions pgs_web/external_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
STYLES_URLS = {
'bootstrap': 'https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css',
'bootstrap-table': 'https://unpkg.com/bootstrap-table@1.21.3/dist/bootstrap-table.min.css',
'font-awesome': 'https://use.fontawesome.com/releases/v6.7.2/css/all.css',
'jquery': 'https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css',
'ebi': 'https://ebi.emblstatic.net/web_guidelines/EBI-Icon-fonts/v1.3/fonts.css'
}
65 changes: 63 additions & 2 deletions pgs_web/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
"""

import os
from pgs_web.constants import USEFUL_URLS
from pgs_web.external_urls import STYLES_URLS
from urllib.parse import urlparse


def get_base_url(full_url):
parse_result = urlparse(full_url)
base_url = parse_result.scheme + '://' + parse_result.netloc
return base_url


if not os.getenv('GAE_APPLICATION', None):
app_settings = os.path.join('./', 'app.yaml')
Expand Down Expand Up @@ -95,9 +105,59 @@
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.contrib.messages.middleware.MessageMiddleware'
]

# ----------------------------- #
# Content Security Policy (CSP) #
# ----------------------------- #
if not DEBUG:
INSTALLED_APPS.append('csp')
MIDDLEWARE.extend([
'csp.middleware.CSPMiddleware',
'catalog.middleware.add_nonce.AddNonceToScriptsMiddleware'
])

CSP_INCLUDE_NONCE_IN = [
'script-src'
]
# default-src
# "strict-dynamic" allows trusted (with nonce) resources to load additional external resources.
CSP_DEFAULT_SRC = ("'self'", "'strict-dynamic'")
# base-uri
CSP_BASE_URI = "'self'"
# frame-ancestors
CSP_FRAME_ANCESTORS = ("'self'",
USEFUL_URLS['EBI_URL'], # Allowing the PGS Catalog to be shown in an EBI training iframe
)
# style-src
# We can't include the CSP nonce into style-src as the templates contain a lot of inline styles within attributes.
# It is therefore necessary to specify all trusted style sources explicitly, including those called from external resources.
CSP_STYLE_SRC = ("'self'",
"'unsafe-inline'",
*(get_base_url(url) for url in STYLES_URLS.values())
)
# script-src
CSP_SCRIPT_SRC = ("'self'",
"'strict-dynamic'",
# The following are only here for backward compatibility with old browsers, as they are unsafe.
# Modern browsers support nonce and "strict-dynamic", which makes them ignore the following.
"'unsafe-inline'",
"http:",
"https:"
)
# img-src
CSP_IMG_SRC = ("'self'",
"data:" # For SVG images
)
# front-src
CSP_FONT_SRC = ("'self'",
get_base_url(STYLES_URLS['font-awesome']),
get_base_url(STYLES_URLS['ebi']))
# connect-src
CSP_CONNECT_SRC = ("'self'",
USEFUL_URLS['EBI_URL'])

# Live middleware
if PGS_ON_LIVE_SITE:
MIDDLEWARE.insert(2, 'corsheaders.middleware.CorsMiddleware')
Expand All @@ -115,6 +175,7 @@
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'catalog.context_processors.pgs_urls',
'catalog.context_processors.styles_urls',
'catalog.context_processors.pgs_settings',
'catalog.context_processors.pgs_search_examples',
'catalog.context_processors.pgs_info',
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ google-auth==2.16.1
google-auth-oauthlib==1.0.0
google-cloud-core==2.3.2
google-cloud-storage==2.7.0
#### Content Security Policy (CSP)
django-csp==3.8

0 comments on commit a951d15

Please sign in to comment.