From 55340dbc4b0c8f7a8b6c7613dcaacf800e8d714d Mon Sep 17 00:00:00 2001 From: alexjanousekGSA Date: Thu, 31 Jul 2025 11:39:58 -0400 Subject: [PATCH] Updated based on scan results --- app/__init__.py | 9 +++----- tests/app/main/views/test_headers.py | 5 +++- tests/app/test_zap_security_fixes.py | 34 ++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 tests/app/test_zap_security_fixes.py diff --git a/app/__init__.py b/app/__init__.py index fe6ba515d..875aaab23 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -163,7 +163,6 @@ def _csp(config): "script-src": [ "'self'", asset_domain, - "'unsafe-eval'", "https://js-agent.newrelic.com", "https://gov-bam.nr-data.net", "https://www.googletagmanager.com", @@ -185,11 +184,9 @@ def _csp(config): def create_app(application): @application.after_request - def add_csp_header(response): - existing_csp = response.headers.get("Content-Security-Policy", "") - response.headers["Content-Security-Policy"] = ( - existing_csp + "; form-action 'self';" - ) + def add_security_headers(response): + # Add Cross-Origin-Embedder-Policy header + response.headers["Cross-Origin-Embedder-Policy"] = "credentialless" return response @application.context_processor diff --git a/tests/app/main/views/test_headers.py b/tests/app/main/views/test_headers.py index 33ef2c540..68bfbc73b 100644 --- a/tests/app/main/views/test_headers.py +++ b/tests/app/main/views/test_headers.py @@ -18,7 +18,7 @@ def test_owasp_useful_headers_set( assert search(r"frame-ancestors 'none';", csp) assert search(r"form-action 'self';", csp) assert search( - r"script-src 'self' static\.example\.com 'unsafe-eval' https:\/\/js-agent\.new" + r"script-src 'self' static\.example\.com https:\/\/js-agent\.new" r"relic\.com https:\/\/gov-bam\.nr-data\.net https:\/\/www\.googletagmanager\." r"com https:\/\/www\.google-analytics\.com https:\/\/dap\.digitalgov\.gov " r"https:\/\/cdn\.socket\.io", @@ -50,3 +50,6 @@ def test_owasp_useful_headers_set( ), f"Missing sources in connect-src: {expected_sources - actual_sources}" assert search(r"style-src 'self' static\.example\.com 'nonce-.*';", csp) assert search(r"img-src 'self' static\.example\.com", csp) + + # Test for Cross-Origin-Embedder-Policy header + assert response.headers["Cross-Origin-Embedder-Policy"] == "credentialless" diff --git a/tests/app/test_zap_security_fixes.py b/tests/app/test_zap_security_fixes.py new file mode 100644 index 000000000..089d9bde2 --- /dev/null +++ b/tests/app/test_zap_security_fixes.py @@ -0,0 +1,34 @@ +"""Simple tests to verify ZAP security fixes""" +import pytest + + +def test_csp_no_unsafe_eval(client_request, mocker, mock_get_service_and_organization_counts): + """Check that unsafe-eval was removed from CSP""" + mocker.patch("app.notify_client.user_api_client.UserApiClient.deactivate_user") + client_request.logout() + response = client_request.get_response('.index') + csp = response.headers.get('Content-Security-Policy', '') + + assert "'unsafe-eval'" not in csp + + +def test_no_duplicate_form_action(client_request, mocker, mock_get_service_and_organization_counts): + """Check that form-action only appears once in CSP""" + mocker.patch("app.notify_client.user_api_client.UserApiClient.deactivate_user") + client_request.logout() + response = client_request.get_response('.index') + csp = response.headers.get('Content-Security-Policy', '') + + # Count how many times form-action appears + count = csp.count('form-action') + assert count == 1 + + +def test_cross_origin_embedder_policy_exists(client_request, mocker, mock_get_service_and_organization_counts): + """Check that Cross-Origin-Embedder-Policy header is present""" + mocker.patch("app.notify_client.user_api_client.UserApiClient.deactivate_user") + client_request.logout() + response = client_request.get_response('.index') + + assert 'Cross-Origin-Embedder-Policy' in response.headers + assert response.headers['Cross-Origin-Embedder-Policy'] == 'credentialless'