Skip to content

Commit

Permalink
Add csp_nonce for inline script and gov_template
Browse files Browse the repository at this point in the history
  • Loading branch information
spatel033 committed Dec 11, 2024
1 parent 7d36385 commit 674ec4d
Show file tree
Hide file tree
Showing 5 changed files with 29 additions and 6 deletions.
11 changes: 10 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import pathlib
import secrets
from collections.abc import Callable
from time import monotonic

Expand Down Expand Up @@ -222,6 +223,7 @@ def init_app(application):
application.before_request(load_user_id_before_request)
application.before_request(load_service_before_request)
application.before_request(load_organisation_before_request)
application.before_request(make_nonce_before_request)

application.session_interface = NotifyAdminSessionInterface()

Expand Down Expand Up @@ -336,6 +338,12 @@ def load_user_id_before_request():
g.user_id = get_user_id_from_flask_login_session()


def make_nonce_before_request():
# `govuk_frontend_jinja/template.html` can be extended and inline `<script>` can be added without CSP complaining
if not getattr(request, "csp_nonce", None):
request.csp_nonce = secrets.token_urlsafe(16)


# https://www.owasp.org/index.php/List_of_useful_HTTP_headers
def useful_headers_after_request(response):
response.headers.add("X-Content-Type-Options", "nosniff")
Expand All @@ -344,7 +352,7 @@ def useful_headers_after_request(response):
"Content-Security-Policy",
(
"default-src 'self' {asset_domain} 'unsafe-inline';"
"script-src 'self' {asset_domain} 'unsafe-inline' 'unsafe-eval' data:;"
"script-src 'self' {asset_domain} 'nonce-{csp_nonce}';"
"connect-src 'self';"
"object-src 'self';"
"font-src 'self' {asset_domain} data:;"
Expand All @@ -355,6 +363,7 @@ def useful_headers_after_request(response):
"frame-src 'self';".format(
asset_domain=current_app.config["ASSET_DOMAIN"],
logo_domain=current_app.config["LOGO_CDN_DOMAIN"],
csp_nonce=getattr(request, "csp_nonce", ""),
)
),
)
Expand Down
6 changes: 4 additions & 2 deletions app/templates/admin_template.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{% extends "govuk_frontend_jinja/template.html"%}

{% set cspNonce = request.csp_nonce %}

{% block headIcons %}
<link rel="icon" sizes="48x48" href="{{ asset_url('images/favicon.ico') }}">
<link rel="icon" sizes="any" href="{{ asset_url('images/favicon.svg') }}" type="image/svg+xml">
Expand Down Expand Up @@ -133,6 +135,6 @@
{% block extra_javascripts %}
{% endblock %}

<script type="module" src="{{ asset_url('javascripts/all-esm.mjs') }}"></script>
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
<script nonce="{{ request.csp_nonce }}" type="module" src="{{ asset_url('javascripts/all-esm.mjs') }}"></script>
<script nonce="{{ request.csp_nonce }}" type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
{% endblock %}
2 changes: 1 addition & 1 deletion app/templates/views/email-link-interstitial.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

</div>

<script type="text/javascript">
<script nonce="{{ request.csp_nonce }}" type="text/javascript">
document.getElementById("use-email-auth").submit();
</script>

Expand Down
11 changes: 9 additions & 2 deletions tests/app/main/views/test_headers.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
def test_owasp_useful_headers_set(
client_request,
mocker,
mock_get_service_and_organisation_counts,
mock_get_letter_rates,
mock_get_sms_rate,
fake_nonce,
):
mocker.patch("secrets.token_urlsafe", return_value=fake_nonce)

client_request.logout()
response = client_request.get_response(".index")

assert response.headers["X-Content-Type-Options"] == "nosniff"
assert response.headers["X-XSS-Protection"] == "1; mode=block"
assert response.headers["Content-Security-Policy"] == (
"default-src 'self' static.example.com 'unsafe-inline';"
"script-src 'self' static.example.com 'unsafe-inline' 'unsafe-eval' data:;"
"script-src 'self' static.example.com 'nonce-TESTs5Vr8v3jgRYLoQuVwA';"
"connect-src 'self';"
"object-src 'self';"
"font-src 'self' static.example.com data:;"
Expand Down Expand Up @@ -43,7 +47,10 @@ def test_headers_non_ascii_characters_are_replaced(
mock_get_service_and_organisation_counts,
mock_get_letter_rates,
mock_get_sms_rate,
fake_nonce,
):
mocker.patch("secrets.token_urlsafe", return_value=fake_nonce)

client_request.logout()
mocker.patch.dict(
"app.current_app.config",
Expand All @@ -54,7 +61,7 @@ def test_headers_non_ascii_characters_are_replaced(

assert response.headers["Content-Security-Policy"] == (
"default-src 'self' static.example.com 'unsafe-inline';"
"script-src 'self' static.example.com 'unsafe-inline' 'unsafe-eval' data:;"
"script-src 'self' static.example.com 'nonce-TESTs5Vr8v3jgRYLoQuVwA';"
"connect-src 'self';"
"object-src 'self';"
"font-src 'self' static.example.com data:;"
Expand Down
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4374,3 +4374,8 @@ def mock_get_notifications_count_for_service(mocker):
"app.notification_api_client.get_notifications_count_for_service",
return_value=100,
)


@pytest.fixture(scope="function")
def fake_nonce():
return "TESTs5Vr8v3jgRYLoQuVwA"

0 comments on commit 674ec4d

Please sign in to comment.