From 5ae8d5462c103b59111ddfbfd88c510e8b591a07 Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Tue, 9 Dec 2025 18:35:57 -0500 Subject: [PATCH 01/18] Barebones header applier. --- src/fides/api/app_setup.py | 2 + src/fides/api/util/security_headers.py | 99 ++++++++++++++++++++++++++ src/fides/config/security_settings.py | 6 +- 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/fides/api/util/security_headers.py diff --git a/src/fides/api/app_setup.py b/src/fides/api/app_setup.py index 748e7e86cc4..09701a14090 100644 --- a/src/fides/api/app_setup.py +++ b/src/fides/api/app_setup.py @@ -52,6 +52,7 @@ fides_limiter, is_rate_limit_enabled, ) +from fides.api.util.security_headers import SecurityHeadersMiddleware from fides.api.util.saas_config_updater import update_saas_configs from fides.config import CONFIG from fides.config.config_proxy import ConfigProxy @@ -87,6 +88,7 @@ def create_fides_app( fastapi_app.state.limiter = fides_limiter # Starlette bug causing this to fail mypy fastapi_app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore + fastapi_app.add_middleware(SecurityHeadersMiddleware) for handler in ExceptionHandlers.get_handlers(): # Starlette bug causing this to fail mypy fastapi_app.add_exception_handler(RedisNotConfigured, handler) # type: ignore diff --git a/src/fides/api/util/security_headers.py b/src/fides/api/util/security_headers.py new file mode 100644 index 00000000000..1a1b1690c68 --- /dev/null +++ b/src/fides/api/util/security_headers.py @@ -0,0 +1,99 @@ +import re +from dataclasses import dataclass +from typing import TypeAlias + +from fastapi import Request +from starlette.middleware.base import BaseHTTPMiddleware + +from fides.config import CONFIG + +apply_recommended_headers = CONFIG.security.headers_mode == "recommended" + + +def is_exact_match(matcher: re.Pattern[str], path_name: str) -> bool: + matched_content = re.match(matcher, path_name) + is_included_path = matched_content is not None and len( + matched_content.string + ) == len(path_name) + + return is_included_path + + +HeaderDefinition: TypeAlias = tuple[str, str] + + +@dataclass +class HeaderRule: + matcher: re.Pattern[str] + headers: list[HeaderDefinition] + + +recommended_headers: list[HeaderRule] = [ + HeaderRule( + matcher=re.compile(r"/.*"), + headers=[ + ("X-Content-Type-Options", "nosniff"), + ("Strict-Transport-Security", "max-age=31536000"), + ], + ), + HeaderRule( + matcher=re.compile(r"/((?!api|health).*)"), + headers=[ + ( + "Content-Security-Policy", + re.sub( + r"\s{2,}", + " ", + """" + default-src 'self'; + script-src 'self' 'unsafe-inline'; + style-src 'self' 'unsafe-inline'; + connect-src 'self'; + img-src 'self' blob: data:; + font-src 'self'; + object-src 'none'; + base-uri 'self'; + form-action 'self'; + frame-ancestors 'self'; + upgrade-insecure-requests; + """, + ), + ) + ], + ), +] + + +def get_applicable_header_rules( + path: str, header_rules: list[HeaderRule] +) -> list[HeaderDefinition]: + header_names: set[str] = set() + header_definitions: list[HeaderDefinition] = [] + + for rule in header_rules: + if is_exact_match(rule.matcher, path): + for header in rule.headers: + [header_name, _] = header + if not header_name in header_names: + header_names.add(header_name) + header_definitions.append(header) + + return header_definitions + + +class SecurityHeadersMiddleware(BaseHTTPMiddleware): + """ + Controls what security headers are included in Fides API responses + """ + + async def dispatch(self, request: Request, call_next): # type: ignore + response = await call_next(request) + + if apply_recommended_headers: + applicable_headers = get_applicable_header_rules( + request.url.path, recommended_headers + ) + for [header_name, header_value] in applicable_headers: + response.headers.append(header_name, header_value) + + return response diff --git a/src/fides/config/security_settings.py b/src/fides/config/security_settings.py index f62cf61708a..33c2642b7ce 100644 --- a/src/fides/config/security_settings.py +++ b/src/fides/config/security_settings.py @@ -1,7 +1,7 @@ """This module handles finding and parsing fides configuration files.""" # pylint: disable=C0115,C0116, E0213 -from typing import List, Optional, Pattern, Tuple, Union +from typing import List, Literal, Optional, Pattern, Tuple, Union from pydantic import Field, SerializeAsAny, ValidationInfo, field_validator from pydantic_settings import SettingsConfigDict @@ -97,6 +97,10 @@ class SecuritySettings(FidesSettings): default=None, description="The header used to determine the client IP address for rate limiting. If not set or set to empty string, rate limiting will be disabled.", ) + headers_mode: Union[Literal["none"], Literal["recommended"]] = Field( + default="none", + description="Controls what security headers are included in Fides server responses.", + ) request_rate_limit: str = Field( default="2000/minute", description="The number of requests from a single IP address allowed to hit an endpoint within a rolling 60 second period.", From 6088101b842730b9a00bb8890870f891385d5738 Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Tue, 9 Dec 2025 18:58:02 -0500 Subject: [PATCH 02/18] Header changes. --- src/fides/api/util/security_headers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fides/api/util/security_headers.py b/src/fides/api/util/security_headers.py index 1a1b1690c68..5274b698500 100644 --- a/src/fides/api/util/security_headers.py +++ b/src/fides/api/util/security_headers.py @@ -58,7 +58,8 @@ class HeaderRule: upgrade-insecure-requests; """, ), - ) + ), + ("X-Frame-Options", "SAMEORIGIN"), ], ), ] From f2fa3554858248b9caf7053acb60e12393ea69bb Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Wed, 17 Dec 2025 12:39:37 -0500 Subject: [PATCH 03/18] Import sorting. --- src/fides/api/app_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fides/api/app_setup.py b/src/fides/api/app_setup.py index 09701a14090..38edcd6b040 100644 --- a/src/fides/api/app_setup.py +++ b/src/fides/api/app_setup.py @@ -52,8 +52,8 @@ fides_limiter, is_rate_limit_enabled, ) -from fides.api.util.security_headers import SecurityHeadersMiddleware from fides.api.util.saas_config_updater import update_saas_configs +from fides.api.util.security_headers import SecurityHeadersMiddleware from fides.config import CONFIG from fides.config.config_proxy import ConfigProxy From 1ebad123c559789bde290fc416ed59fcb7ea690a Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Thu, 18 Dec 2025 15:22:57 -0500 Subject: [PATCH 04/18] Python 3.9 doesn't have typealias. --- src/fides/api/util/security_headers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fides/api/util/security_headers.py b/src/fides/api/util/security_headers.py index 5274b698500..62ad4b6a19d 100644 --- a/src/fides/api/util/security_headers.py +++ b/src/fides/api/util/security_headers.py @@ -1,6 +1,5 @@ import re from dataclasses import dataclass -from typing import TypeAlias from fastapi import Request from starlette.middleware.base import BaseHTTPMiddleware @@ -19,7 +18,7 @@ def is_exact_match(matcher: re.Pattern[str], path_name: str) -> bool: return is_included_path -HeaderDefinition: TypeAlias = tuple[str, str] +HeaderDefinition = tuple[str, str] @dataclass From 3a749b2e885483917b72e814e3ce39692d0604f2 Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Mon, 5 Jan 2026 16:03:56 -0500 Subject: [PATCH 05/18] Unit testing. --- src/fides/api/util/security_headers.py | 16 +++++--- tests/api/util/test_security_headers.py | 53 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 tests/api/util/test_security_headers.py diff --git a/src/fides/api/util/security_headers.py b/src/fides/api/util/security_headers.py index 62ad4b6a19d..d5670c25127 100644 --- a/src/fides/api/util/security_headers.py +++ b/src/fides/api/util/security_headers.py @@ -1,7 +1,7 @@ import re from dataclasses import dataclass -from fastapi import Request +from fastapi import Request, Response from starlette.middleware.base import BaseHTTPMiddleware from fides.config import CONFIG @@ -81,6 +81,14 @@ def get_applicable_header_rules( return header_definitions +def apply_headers_to_response(request: Request, response: Response) -> None: + applicable_headers = get_applicable_header_rules( + request.url.path, recommended_headers + ) + for [header_name, header_value] in applicable_headers: + response.headers.append(header_name, header_value) + + class SecurityHeadersMiddleware(BaseHTTPMiddleware): """ Controls what security headers are included in Fides API responses @@ -90,10 +98,6 @@ async def dispatch(self, request: Request, call_next): # type: ignore response = await call_next(request) if apply_recommended_headers: - applicable_headers = get_applicable_header_rules( - request.url.path, recommended_headers - ) - for [header_name, header_value] in applicable_headers: - response.headers.append(header_name, header_value) + apply_headers_to_response(request, response) return response diff --git a/tests/api/util/test_security_headers.py b/tests/api/util/test_security_headers.py new file mode 100644 index 00000000000..3527eb4d66e --- /dev/null +++ b/tests/api/util/test_security_headers.py @@ -0,0 +1,53 @@ +import re +import pytest + +from fides.api.util.security_headers import ( + HeaderRule, + is_exact_match, + get_applicable_header_rules, +) +from fides.config import CONFIG + + +@pytest.fixture(scope="function") +def set_security_headers(db): + """Enable recommended security header mode""" + original_value = CONFIG.security.headers_mode + CONFIG.security.headers_mode = "recommended" + yield + CONFIG.security.headers_mode = original_value + + +class TestSecurityHeaders: + def test_is_exact_match(self): + assert is_exact_match(re.compile(r"\/example-path"), "/example-path") is True + assert ( + is_exact_match( + re.compile(r"\/example-path"), "/example-path/with-more-content" + ) + is True + ) + assert ( + is_exact_match(re.compile(r"\/example-path"), "/anti-example-path") is False + ) + + def test_get_applicable_header_rules_returns_first_matching_rule_for_path(self): + expected_headers: tuple[str, str] = ("header-1", "value-1") + headers: list[HeaderRule] = [ + HeaderRule(re.compile(r"\/a"), [expected_headers]), + HeaderRule(re.compile(r"\/a"), [("header-1", "value-2")]), + ] + + assert get_applicable_header_rules("/a", headers) == [expected_headers] + + def test_get_applicable_header_rules_returns_disparate_headers(self): + header1 = "header-1" + header2 = "header-2" + headers1: tuple[str, str] = (header1, "value-1") + headers2: tuple[str, str] = (header2, "value-2") + headers: list[HeaderRule] = [ + HeaderRule(re.compile(r"\/a-path"), [headers1]), + HeaderRule(re.compile(r"\/a-path"), [headers2]), + ] + + assert get_applicable_header_rules("/a-path", headers) == [headers1, headers2] From e347ec39407ce5db397bb17412b36b28f2294d90 Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Mon, 5 Jan 2026 16:21:27 -0500 Subject: [PATCH 06/18] Sort imports. --- tests/api/util/test_security_headers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/api/util/test_security_headers.py b/tests/api/util/test_security_headers.py index 3527eb4d66e..84c1413a62a 100644 --- a/tests/api/util/test_security_headers.py +++ b/tests/api/util/test_security_headers.py @@ -1,10 +1,11 @@ import re + import pytest from fides.api.util.security_headers import ( HeaderRule, - is_exact_match, get_applicable_header_rules, + is_exact_match, ) from fides.config import CONFIG From bbeb867d9f1c53754eaa17f1968d453df892d699 Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Mon, 5 Jan 2026 17:06:49 -0500 Subject: [PATCH 07/18] Fix exact match check. --- src/fides/api/util/security_headers.py | 10 ++++------ tests/api/util/test_security_headers.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/fides/api/util/security_headers.py b/src/fides/api/util/security_headers.py index d5670c25127..71dfe35f454 100644 --- a/src/fides/api/util/security_headers.py +++ b/src/fides/api/util/security_headers.py @@ -11,11 +11,9 @@ def is_exact_match(matcher: re.Pattern[str], path_name: str) -> bool: matched_content = re.match(matcher, path_name) - is_included_path = matched_content is not None and len( - matched_content.string - ) == len(path_name) - - return is_included_path + if matched_content is None: + return False + return len(matched_content.group(0)) == len(path_name) HeaderDefinition = tuple[str, str] @@ -74,7 +72,7 @@ def get_applicable_header_rules( if is_exact_match(rule.matcher, path): for header in rule.headers: [header_name, _] = header - if not header_name in header_names: + if header_name not in header_names: header_names.add(header_name) header_definitions.append(header) diff --git a/tests/api/util/test_security_headers.py b/tests/api/util/test_security_headers.py index 84c1413a62a..69c63200797 100644 --- a/tests/api/util/test_security_headers.py +++ b/tests/api/util/test_security_headers.py @@ -26,11 +26,22 @@ def test_is_exact_match(self): is_exact_match( re.compile(r"\/example-path"), "/example-path/with-more-content" ) + is False + ) + assert ( + is_exact_match( + re.compile(r"\/example-path/?(.*)"), "/example-path/with-more-content" + ) is True ) assert ( is_exact_match(re.compile(r"\/example-path"), "/anti-example-path") is False ) + assert ( + is_exact_match( + re.compile(r"\/example-path"), "/completely-disparate-no-match" + ) + ) is False def test_get_applicable_header_rules_returns_first_matching_rule_for_path(self): expected_headers: tuple[str, str] = ("header-1", "value-1") From f5f9986ab3e7404d75fda34c4a6a0e3b83be14b2 Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Mon, 5 Jan 2026 17:08:30 -0500 Subject: [PATCH 08/18] Tests. --- tests/api/util/test_security_headers.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/api/util/test_security_headers.py b/tests/api/util/test_security_headers.py index 69c63200797..1458f248672 100644 --- a/tests/api/util/test_security_headers.py +++ b/tests/api/util/test_security_headers.py @@ -1,22 +1,11 @@ import re -import pytest from fides.api.util.security_headers import ( HeaderRule, get_applicable_header_rules, is_exact_match, ) -from fides.config import CONFIG - - -@pytest.fixture(scope="function") -def set_security_headers(db): - """Enable recommended security header mode""" - original_value = CONFIG.security.headers_mode - CONFIG.security.headers_mode = "recommended" - yield - CONFIG.security.headers_mode = original_value class TestSecurityHeaders: From 3b7acfdc0227b9465b30411a7079ea2ad5e0eb85 Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Mon, 5 Jan 2026 17:13:40 -0500 Subject: [PATCH 09/18] Exclude header and API paths from CSP. --- src/fides/api/util/security_headers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fides/api/util/security_headers.py b/src/fides/api/util/security_headers.py index 71dfe35f454..81edc322819 100644 --- a/src/fides/api/util/security_headers.py +++ b/src/fides/api/util/security_headers.py @@ -34,7 +34,7 @@ class HeaderRule: ], ), HeaderRule( - matcher=re.compile(r"/((?!api|health).*)"), + matcher=re.compile(r"^/((?!api|health).*)"), headers=[ ( "Content-Security-Policy", From 26820f8237e74c4913c50dfba0cd66fcfb844432 Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Mon, 5 Jan 2026 17:15:56 -0500 Subject: [PATCH 10/18] Update imports. --- tests/api/util/test_security_headers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/api/util/test_security_headers.py b/tests/api/util/test_security_headers.py index 1458f248672..25592d2a43e 100644 --- a/tests/api/util/test_security_headers.py +++ b/tests/api/util/test_security_headers.py @@ -1,6 +1,5 @@ import re - from fides.api.util.security_headers import ( HeaderRule, get_applicable_header_rules, From c87903cc5eb203d6b728a7f550918a27e27bc076 Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Tue, 6 Jan 2026 10:56:20 -0500 Subject: [PATCH 11/18] More testing. --- src/fides/api/util/security_headers.py | 47 +++++++++++---------- tests/api/util/test_security_headers.py | 55 +++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 22 deletions(-) diff --git a/src/fides/api/util/security_headers.py b/src/fides/api/util/security_headers.py index 81edc322819..bc557b491c6 100644 --- a/src/fides/api/util/security_headers.py +++ b/src/fides/api/util/security_headers.py @@ -25,6 +25,24 @@ class HeaderRule: headers: list[HeaderDefinition] +recommended_csp_header_value = re.sub( + r"\s{2,}", + " ", + """" + default-src 'self'; + script-src 'self' 'unsafe-inline'; + style-src 'self' 'unsafe-inline'; + connect-src 'self'; + img-src 'self' blob: data:; + font-src 'self'; + object-src 'none'; + base-uri 'self'; + form-action 'self'; + frame-ancestors 'self'; + upgrade-insecure-requests; + """, +) + recommended_headers: list[HeaderRule] = [ HeaderRule( matcher=re.compile(r"/.*"), @@ -38,23 +56,7 @@ class HeaderRule: headers=[ ( "Content-Security-Policy", - re.sub( - r"\s{2,}", - " ", - """" - default-src 'self'; - script-src 'self' 'unsafe-inline'; - style-src 'self' 'unsafe-inline'; - connect-src 'self'; - img-src 'self' blob: data:; - font-src 'self'; - object-src 'none'; - base-uri 'self'; - form-action 'self'; - frame-ancestors 'self'; - upgrade-insecure-requests; - """, - ), + recommended_csp_header_value, ), ("X-Frame-Options", "SAMEORIGIN"), ], @@ -79,12 +81,13 @@ def get_applicable_header_rules( return header_definitions -def apply_headers_to_response(request: Request, response: Response) -> None: - applicable_headers = get_applicable_header_rules( - request.url.path, recommended_headers - ) +def apply_headers_to_response( + headers: list[HeaderRule], request: Request, response: Response +) -> None: + applicable_headers = get_applicable_header_rules(request.url.path, headers) for [header_name, header_value] in applicable_headers: response.headers.append(header_name, header_value) + response.headers.keys() class SecurityHeadersMiddleware(BaseHTTPMiddleware): @@ -96,6 +99,6 @@ async def dispatch(self, request: Request, call_next): # type: ignore response = await call_next(request) if apply_recommended_headers: - apply_headers_to_response(request, response) + apply_headers_to_response(recommended_headers, request, response) return response diff --git a/tests/api/util/test_security_headers.py b/tests/api/util/test_security_headers.py index 25592d2a43e..97ffed8db62 100644 --- a/tests/api/util/test_security_headers.py +++ b/tests/api/util/test_security_headers.py @@ -1,9 +1,16 @@ import re +from unittest import mock + +import pytest +from fastapi import Request, Response from fides.api.util.security_headers import ( HeaderRule, + apply_headers_to_response, get_applicable_header_rules, is_exact_match, + recommended_csp_header_value, + recommended_headers, ) @@ -51,3 +58,51 @@ def test_get_applicable_header_rules_returns_disparate_headers(self): ] assert get_applicable_header_rules("/a-path", headers) == [headers1, headers2] + + def test_apply_headers_to_response(self): + header = ("header-1", "value-1") + header_rules: list[HeaderRule] = [HeaderRule(re.compile(r".*"), [header])] + + mock_request = mock.Mock(spec=Request) + mock_request.url.path = "/any-path" + + response = Response() + + apply_headers_to_response(header_rules, mock_request, response) + + assert header in response.headers.items() + + @pytest.mark.parametrize( + "path,expected", + [ + ( + "/api/foo", + [ + ("X-Content-Type-Options", "nosniff"), + ("Strict-Transport-Security", "max-age=31536000"), + ], + ), + ( + "/health", + [ + ("X-Content-Type-Options", "nosniff"), + ("Strict-Transport-Security", "max-age=31536000"), + ], + ), + ( + "/privacy-requests", + [ + ("X-Content-Type-Options", "nosniff"), + ("Strict-Transport-Security", "max-age=31536000"), + ( + "Content-Security-Policy", + recommended_csp_header_value, + ), + ("X-Frame-Options", "SAMEORIGIN"), + ], + ), + ], + ) + def test_recommended_headers_api_route(self, path, expected): + applicable_rules = get_applicable_header_rules(path, recommended_headers) + assert applicable_rules == expected From 8bd609391aa191c7e7078b49c180cc9bb4078ab4 Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Tue, 6 Jan 2026 11:04:04 -0500 Subject: [PATCH 12/18] Remove errant line. --- src/fides/api/util/security_headers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fides/api/util/security_headers.py b/src/fides/api/util/security_headers.py index bc557b491c6..551f7be1171 100644 --- a/src/fides/api/util/security_headers.py +++ b/src/fides/api/util/security_headers.py @@ -87,7 +87,6 @@ def apply_headers_to_response( applicable_headers = get_applicable_header_rules(request.url.path, headers) for [header_name, header_value] in applicable_headers: response.headers.append(header_name, header_value) - response.headers.keys() class SecurityHeadersMiddleware(BaseHTTPMiddleware): From 88d4a3be33ccad56532517adb6cf4bed44781bef Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Tue, 6 Jan 2026 12:32:42 -0500 Subject: [PATCH 13/18] Parametrize test. --- tests/api/util/test_security_headers.py | 34 +++++++++---------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/tests/api/util/test_security_headers.py b/tests/api/util/test_security_headers.py index 97ffed8db62..6e0cb7926df 100644 --- a/tests/api/util/test_security_headers.py +++ b/tests/api/util/test_security_headers.py @@ -15,28 +15,18 @@ class TestSecurityHeaders: - def test_is_exact_match(self): - assert is_exact_match(re.compile(r"\/example-path"), "/example-path") is True - assert ( - is_exact_match( - re.compile(r"\/example-path"), "/example-path/with-more-content" - ) - is False - ) - assert ( - is_exact_match( - re.compile(r"\/example-path/?(.*)"), "/example-path/with-more-content" - ) - is True - ) - assert ( - is_exact_match(re.compile(r"\/example-path"), "/anti-example-path") is False - ) - assert ( - is_exact_match( - re.compile(r"\/example-path"), "/completely-disparate-no-match" - ) - ) is False + @pytest.mark.parametrize( + "pattern,path,expected", + [ + (r"\/example-path", "/example-path", True), + (r"\/example-path", "/example-path/with-more-content", False), + (r"\/example-path/?(.*)", "/example-path/with-more-content", True), + (r"\/example-path", "/anti-example-path", False), + (r"\/example-path", "/completely-disparate-no-match", False), + ], + ) + def test_is_exact_match(self, pattern, path, expected): + assert (is_exact_match(pattern, path)) is expected def test_get_applicable_header_rules_returns_first_matching_rule_for_path(self): expected_headers: tuple[str, str] = ("header-1", "value-1") From 1791e3d8a32ec175b4914b76d73f0af8fbfb34ea Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Tue, 6 Jan 2026 15:27:26 -0500 Subject: [PATCH 14/18] fullmatch --- src/fides/api/util/security_headers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/fides/api/util/security_headers.py b/src/fides/api/util/security_headers.py index 551f7be1171..671344b67a5 100644 --- a/src/fides/api/util/security_headers.py +++ b/src/fides/api/util/security_headers.py @@ -10,10 +10,8 @@ def is_exact_match(matcher: re.Pattern[str], path_name: str) -> bool: - matched_content = re.match(matcher, path_name) - if matched_content is None: - return False - return len(matched_content.group(0)) == len(path_name) + matched_content = re.fullmatch(matcher, path_name) + return matched_content is not None HeaderDefinition = tuple[str, str] From 00c76c825303daabf75cbac30f49a0af57e514cb Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Tue, 6 Jan 2026 15:28:01 -0500 Subject: [PATCH 15/18] Remove extra quote. --- src/fides/api/util/security_headers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fides/api/util/security_headers.py b/src/fides/api/util/security_headers.py index 671344b67a5..711d3060cc9 100644 --- a/src/fides/api/util/security_headers.py +++ b/src/fides/api/util/security_headers.py @@ -26,7 +26,7 @@ class HeaderRule: recommended_csp_header_value = re.sub( r"\s{2,}", " ", - """" + """ default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; From 032586e6e82225852fc83bd3ce62c73b893e7b27 Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Tue, 6 Jan 2026 15:29:06 -0500 Subject: [PATCH 16/18] Strip --- src/fides/api/util/security_headers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fides/api/util/security_headers.py b/src/fides/api/util/security_headers.py index 711d3060cc9..7bce2266b4b 100644 --- a/src/fides/api/util/security_headers.py +++ b/src/fides/api/util/security_headers.py @@ -39,7 +39,7 @@ class HeaderRule: frame-ancestors 'self'; upgrade-insecure-requests; """, -) +).strip() recommended_headers: list[HeaderRule] = [ HeaderRule( From 741bd326e7edce9bfb64ba0337ca7c62bc6f0e38 Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Tue, 6 Jan 2026 15:32:35 -0500 Subject: [PATCH 17/18] Compile test regex. --- tests/api/util/test_security_headers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/util/test_security_headers.py b/tests/api/util/test_security_headers.py index 6e0cb7926df..c2970ea4444 100644 --- a/tests/api/util/test_security_headers.py +++ b/tests/api/util/test_security_headers.py @@ -26,7 +26,7 @@ class TestSecurityHeaders: ], ) def test_is_exact_match(self, pattern, path, expected): - assert (is_exact_match(pattern, path)) is expected + assert (is_exact_match(re.compile(pattern), path)) is expected def test_get_applicable_header_rules_returns_first_matching_rule_for_path(self): expected_headers: tuple[str, str] = ("header-1", "value-1") From 87bcb2a9c716a03a94ce9bdbd76cb7efae59dc9c Mon Sep 17 00:00:00 2001 From: Thomas Van Dort Date: Tue, 6 Jan 2026 15:50:36 -0500 Subject: [PATCH 18/18] One more test case for clarity. --- tests/api/util/test_security_headers.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/api/util/test_security_headers.py b/tests/api/util/test_security_headers.py index c2970ea4444..761c34d0505 100644 --- a/tests/api/util/test_security_headers.py +++ b/tests/api/util/test_security_headers.py @@ -91,6 +91,18 @@ def test_apply_headers_to_response(self): ("X-Frame-Options", "SAMEORIGIN"), ], ), + ( + "/", + [ + ("X-Content-Type-Options", "nosniff"), + ("Strict-Transport-Security", "max-age=31536000"), + ( + "Content-Security-Policy", + recommended_csp_header_value, + ), + ("X-Frame-Options", "SAMEORIGIN"), + ], + ), ], ) def test_recommended_headers_api_route(self, path, expected):