Skip to content

Commit

Permalink
1081 rh UI implement security middleware and security policies (#7)
Browse files Browse the repository at this point in the history
* Security policy

* Fixed security headers setup

* Code review changes

* Removed commented out code

* Removded HTTPS toggle

* Added docstring to headder setting method

* Fixed integration test
  • Loading branch information
KieranWardle authored Oct 17, 2023
1 parent f0483f1 commit 4838420
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 15 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ structlog = "*"
flask-babel = "*"
requests = "*"
gunicorn = "*"
flask-talisman = "*"

[dev-packages]
flake8 = "*"
Expand Down
16 changes: 12 additions & 4 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions rh_ui/app_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from flask import Flask, g, request
from flask_babel import Babel
from structlog import wrap_logger
from flask_talisman import Talisman

from rh_ui.security import CSP, PERMISSION_POLICY
from rh_ui.logger_config import logger_initial_config


Expand Down Expand Up @@ -40,5 +42,20 @@ def get_locale() -> str:
app.register_error_handler(404, handle_404)
from rh_ui.views.error_handlers import handle_500
app.register_error_handler(500, handle_500)
# Register security
from rh_ui.security import security
app.register_blueprint(security)

Talisman(
app,
content_security_policy=CSP,
content_security_policy_nonce_in=['script-src'],
force_https=False,
frame_options='DENY',
strict_transport_security='includeSubDomains',
strict_transport_security_max_age=31536000,
x_content_type_options='nosniff',
permissions_policy=PERMISSION_POLICY,
)

return app
71 changes: 71 additions & 0 deletions rh_ui/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from flask import Blueprint
CSP = {
'default-src': [
"'self'",
'https://cdn.ons.gov.uk',
],
'font-src': [
"'self'",
'data:',
'https://cdn.ons.gov.uk',
],
'script-src': [
"'self'",
'https://*.googletagmanager.com',
'https://cdn.ons.gov.uk',
],
'connect-src': [
"'self'",
'https://cdn.ons.gov.uk',
'https://*.google-analytics.com',
'https://*.analytics.google.com',
'https://*.googletagmanager.com'
],
'img-src': [
"'self'",
'data:',
'https://*.google-analytics.com',
'https://*.googletagmanager.com',
'https://cdn.ons.gov.uk'
],
}

PERMISSION_POLICY = ('accelerometer=(),autoplay=(),camera=(),display-capture=(),document-domain=(),'
'encrypted-media=(),fullscreen=(),geolocation=(),gyroscope=(),magnetometer=(),'
'microphone=(),midi=(),payment=(),picture-in-picture=(),publickey-credentials-get=(),'
'screen-wake-lock=(),sync-xhr=(self),usb=(),xr-spatial-tracking=()')

# These headers were in use on the old RH-UI but are currently not able to be set via Talisman
# and so require a different method to be set
DEFAULT_RESPONSE_HEADERS = {
'X-Frame-Options': 'DENY',
'X-Permitted-Cross-Domain-Policies': 'None',
'clear-site-data': '"storage"',
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Resource-Policy': 'same-site',
'Cache-Control': ['no-store', 'max-age=0'],
'Server': 'Office For National Statistics',
}

security = Blueprint("security", __name__)


def build_response_headers():
headers = {}
for header, value in DEFAULT_RESPONSE_HEADERS.items():
if isinstance(value, dict):
value = '; '.join([
f"{section} {' '.join(content)}"
for section, content in value.items()
])
elif not isinstance(value, str):
value = ' '.join(value)
headers[header] = value
return headers


@security.after_app_request
def add_security_headers(resp):
'''This is required to set extra headers that Talisman doesn't support'''
resp.headers.extend(build_response_headers())
return resp
2 changes: 1 addition & 1 deletion rh_ui/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
'titleLogo' : {}
},
'footer': footer_content,
'cspNonce': cspNonce,
'cspNonce': csp_nonce(),
'meta' : {}
} -%}

Expand Down
4 changes: 2 additions & 2 deletions rh_ui/templates/partials/ga.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<!-- Google tag (gtag.js) -->
<script nonce="{{ cspNonce }}" async
<script nonce="{{ csp_nonce() }}" async
src="https://www.googletagmanager.com/gtag/js?id={{ config['GTM_TAG_ID'] }}"></script>

<script nonce="{{ cspNonce }}">
<script nonce="{{ csp_nonce() }}">
var allowTrackingCookies = /^(.*)?\s*'usage':true\s*[^;]+(.*)?$/;

if (document.cookie.match(allowTrackingCookies)) {
Expand Down
2 changes: 1 addition & 1 deletion rh_ui/templates/partials/gtm.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- Google Tag Manager -->
<script nonce="{{ cspNonce }}">
<script nonce="{{ csp_nonce() }}">
var allowTrackingCookies = /^(.*)?\s*'usage':true\s*[^;]+(.*)?$/;

if (document.cookie.match(allowTrackingCookies)) {
Expand Down
1 change: 1 addition & 0 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

app = create_app()


if __name__ == "__main__":
app.run(debug=app.config["DEBUG"], host="0.0.0.0", port=app.config["PORT"])
2 changes: 1 addition & 1 deletion tests/integration/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def populate_firestore_collection_exercise():
"stringValue": "7101b1ce-8034-4bce-bce1-9a1aef67d5cb"
},
"endDate": {
"timestampValue": "2023-09-13T12:18:42.952Z"
"timestampValue": "2030-09-13T12:18:42.952Z"
},
"collectionInstrumentRules": {
"arrayValue": {
Expand Down
14 changes: 8 additions & 6 deletions whitelist.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,21 @@
GTM_CONTAINER_ID # unused variable (config.py:32)
GTM_TAG_ID # unused variable (config.py:33)
DEBUG # unused variable (config.py:37)
RH_SVC_URL # unused variable (config.py:38)
wsgi_app # unused variable (gunicorn.conf.py:9)
bind # unused variable (gunicorn.conf.py:12)
worker_class # unused variable (gunicorn.conf.py:16)
threads # unused variable (gunicorn.conf.py:17)
access_log_format # unused variable (gunicorn.conf.py:21)
accesslog # unused variable (gunicorn.conf.py:25)
logconfig # unused variable (gunicorn.conf.py:28)
_.secret_key # unused attribute (rh_ui/app_setup.py:26)
worker_class # unused variable (gunicorn.conf.py:14)
threads # unused variable (gunicorn.conf.py:16)
access_log_format # unused variable (gunicorn.conf.py:20)
accesslog # unused variable (gunicorn.conf.py:24)
logconfig # unused variable (gunicorn.conf.py:27)
_.secret_key # unused attribute (rh_ui/app_setup.py:28)
TestingConfig # unused class (config.py:36)
add_language_code # unused function (rh_ui/views/i18n.py:19)
pull_lang_code # unused function (rh_ui/views/i18n.py:24)
cookies # unused function (rh_ui/views/info_pages.py:11)
privacy_and_data_protection # unused function (rh_ui/views/info_pages.py:16)
start_get # unused function (rh_ui/views/start.py:22)
add_security_headers # unused function (rh_ui/security.py:67)
info_healthcheck # unused function (rh_ui/views/healthcheck.py:6)
start_post # unused function (rh_ui/views/start.py:27)

0 comments on commit 4838420

Please sign in to comment.