From a6f7f6c8a127bf481444fde45f0898ad934f76fd Mon Sep 17 00:00:00 2001 From: Nandini Date: Fri, 5 Jun 2026 07:57:17 +0530 Subject: [PATCH] import os --- backend/app.py | 17 ++++---- tests/test_security_headers_500.py | 64 ++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 tests/test_security_headers_500.py diff --git a/backend/app.py b/backend/app.py index 4e61ad695..396488623 100644 --- a/backend/app.py +++ b/backend/app.py @@ -544,11 +544,11 @@ def handle_unhandled_exception(e): # 8. Return the Secure JSON Response # Always return a 500 Internal Server Error status code for unhandled exceptions. response = jsonify(response_data) - - # Adding security headers to error responses as defense in depth - response.headers['X-Content-Type-Options'] = 'nosniff' - response.headers['X-Frame-Options'] = 'DENY' - + + # Ensure the full hardened header set is applied consistently + # (including CSP/HSTS/etc.) even for global 500 handlers. + response = _apply_security_headers(response) + return response, 500 # ========================================================================= @@ -625,8 +625,11 @@ def handle_sqlalchemy_exception(e): } response = jsonify(response_data) - response.headers['X-Content-Type-Options'] = 'nosniff' - response.headers['X-Frame-Options'] = 'DENY' + + # Ensure the full hardened header set is applied consistently + # (including CSP/HSTS/etc.) even for global 500 handlers. + response = _apply_security_headers(response) + return response, 500 # ========================================================================= diff --git a/tests/test_security_headers_500.py b/tests/test_security_headers_500.py new file mode 100644 index 000000000..7270bc0b4 --- /dev/null +++ b/tests/test_security_headers_500.py @@ -0,0 +1,64 @@ +import os +import sys + +import pytest + +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'backend')) +from app import app + + +@pytest.fixture +def client(): + app.config['TESTING'] = True + with app.test_client() as test_client: + yield test_client + + +def _assert_full_security_headers(response): + assert response.status_code == 500 + + # Core hardened headers (must match _apply_security_headers) + assert response.headers['Content-Security-Policy'].startswith("default-src 'self'") + assert "frame-ancestors 'none'" in response.headers['Content-Security-Policy'] + assert response.headers['Strict-Transport-Security'] == 'max-age=31536000; includeSubDomains' + assert response.headers['X-Content-Type-Options'] == 'nosniff' + assert response.headers['Referrer-Policy'] == 'strict-origin-when-cross-origin' + assert response.headers['Permissions-Policy'] == 'geolocation=(), microphone=(), camera=()' + assert response.headers['X-Frame-Options'] == 'DENY' + + +def test_unhandled_exception_returns_full_security_headers(client, monkeypatch): + # Force an unhandled exception within a request context + def boom(): + raise Exception('test boom') + + # Temporarily register a route only for this test run + endpoint = 'test_unhandled_exception' + rule = '/api/v1/test-500-unhandled' + + app.add_url_rule(rule, endpoint, boom, methods=['GET']) + try: + response = client.get(rule) + _assert_full_security_headers(response) + finally: + # Flask doesn't expose a clean route removal API; rely on TESTING cleanup. + # In CI, tests run in a fresh process. + pass + + +def test_sqlalchemy_exception_returns_full_security_headers(client): + from sqlalchemy.exc import SQLAlchemyError + + endpoint = 'test_sqlalchemy_exception' + rule = '/api/v1/test-500-sqlalchemy' + + def db_boom(): + raise SQLAlchemyError('test sqlalchemy error') + + app.add_url_rule(rule, endpoint, db_boom, methods=['GET']) + try: + response = client.get(rule) + _assert_full_security_headers(response) + finally: + pass +