Fides JavaScript Injection Vulnerability in Privacy Center URL
Description
Fides is an open-source privacy engineering platform for managing the fulfillment of data privacy requests in runtime environments, helping enforce privacy regulations in code. The Fides web application allows users to edit consent and privacy notices such as cookie banners. The vulnerability makes it possible to craft a payload in the privacy policy URL which triggers JavaScript execution when the privacy notice is served by an integrated website. The domain scope of the executed JavaScript is that of the integrated website. Exploitation is limited to Admin UI users with the contributor role or higher. The vulnerability has been patched in Fides version 2.22.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ethyca-fidesPyPI | < 2.22.1 | 2.22.1 |
Affected products
1Patches
13231d19699f9Merge pull request from GHSA-fgjj-5jmr-gh83
7 files changed · +94 −42
src/fides/api/schemas/privacy_experience.py+3 −3 modified@@ -3,7 +3,7 @@ from datetime import datetime from typing import Any, Dict, List, Optional -from pydantic import Extra, Field, root_validator, validator +from pydantic import Extra, Field, HttpUrl, root_validator, validator from fides.api.models.privacy_experience import BannerEnabled, ComponentType from fides.api.models.privacy_notice import PrivacyNoticeRegion @@ -43,8 +43,8 @@ class ExperienceConfigSchema(FidesSchema): privacy_policy_link_label: Optional[str] = Field( description="Overlay and Privacy Center 'Privacy policy link label'" ) - privacy_policy_url: Optional[str] = Field( - description="Overlay and Privacy Center 'Privacy policy URl'" + privacy_policy_url: Optional[HttpUrl] = Field( + default=None, description="Overlay and Privacy Center 'Privacy policy URL" ) privacy_preferences_link_label: Optional[str] = Field( description="Overlay 'Privacy preferences link label'"
tests/fixtures/application_fixtures.py+3 −3 modified@@ -2302,7 +2302,7 @@ def privacy_preference_history( "request_origin": "privacy_center", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/324.42 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/425.24", "user_geography": "us_ca", - "url_recorded": "example.com/privacy_center", + "url_recorded": "https://example.com/privacy_center", "served_notice_history_id": served_notice_history.id, }, check_name=False, @@ -2651,7 +2651,7 @@ def experience_config_overlay(db: Session) -> Generator: "disabled": False, "privacy_preferences_link_label": "Manage preferences", "privacy_policy_link_label": "View our company's privacy policy", - "privacy_policy_url": "example.com/privacy", + "privacy_policy_url": "https://example.com/privacy", "reject_button_label": "Reject all", "save_button_label": "Save", "title": "Manage your consent", @@ -2677,7 +2677,7 @@ def experience_config_tcf_overlay(db: Session) -> Generator: "disabled": False, "privacy_preferences_link_label": "Manage preferences", "privacy_policy_link_label": "View our company's privacy policy", - "privacy_policy_url": "example.com/privacy", + "privacy_policy_url": "https://example.com/privacy", "reject_button_label": "Reject all", "save_button_label": "Save", "title": "Manage your consent",
tests/ops/api/v1/endpoints/test_privacy_experience_config_endpoints.py+48 −13 modified@@ -317,7 +317,7 @@ def overlay_experience_request_body(self) -> dict: "disabled": False, "privacy_preferences_link_label": "Manage preferences", "privacy_policy_link_label": "View our privacy policy", - "privacy_policy_url": "example.com/privacy", + "privacy_policy_url": "http://example.com/privacy", "reject_button_label": "Reject all", "regions": [], "save_button_label": "Save", @@ -455,7 +455,7 @@ def test_create_another_default_experience_config( "description": "We take your privacy seriously", "is_default": True, "privacy_policy_link_label": "Manage your privacy", - "privacy_policy_url": "example.com/privacy", + "privacy_policy_url": "http://example.com/privacy", "reject_button_label": "No", "save_button_label": "Save", "title": "Manage your privacy", @@ -469,6 +469,39 @@ def test_create_another_default_experience_config( == "Cannot set as the default. Only one default privacy_center config can be in the system." ) + @pytest.mark.parametrize( + "invalid_url", + [ + "thisisnotaurl", + "javascript:alert('XSS: domain scope: '+document.domain)", + ], + ) + def test_create_experience_config_with_invalid_policy_url( + self, api_client: TestClient, url, generate_auth_header, db, invalid_url + ) -> None: + """ + Verify that an invalid Privacy Policy URL returns a 422. + """ + auth_header = generate_auth_header( + scopes=[scopes.PRIVACY_EXPERIENCE_CREATE, scopes.PRIVACY_EXPERIENCE_UPDATE] + ) + response = api_client.post( + url, + json={ + "accept_button_label": "Yes", + "banner_enabled": "always_disabled", + "component": "privacy_center", + "description": "We take your company's privacy seriously", + "privacy_policy_link_label": "Manage your privacy", + "privacy_policy_url": invalid_url, + "reject_button_label": "No", + "save_button_label": "Save", + "title": "Manage your privacy", + }, + headers=auth_header, + ) + assert response.status_code == 422 + def test_create_experience_config_with_no_regions( self, api_client: TestClient, url, generate_auth_header, db ) -> None: @@ -487,7 +520,7 @@ def test_create_experience_config_with_no_regions( "component": "privacy_center", "description": "We take your company's privacy seriously", "privacy_policy_link_label": "Manage your privacy", - "privacy_policy_url": "example.com/privacy", + "privacy_policy_url": "http://example.com/privacy", "reject_button_label": "No", "save_button_label": "Save", "title": "Manage your privacy", @@ -503,7 +536,7 @@ def test_create_experience_config_with_no_regions( resp["description"] == "We take your company's privacy seriously" ) # Returned in the response, unescaped, for display assert resp["privacy_policy_link_label"] == "Manage your privacy" - assert resp["privacy_policy_url"] == "example.com/privacy" + assert resp["privacy_policy_url"] == "http://example.com/privacy" assert resp["regions"] == [] assert resp["reject_button_label"] == "No" assert resp["save_button_label"] == "Save" @@ -551,7 +584,7 @@ def test_create_experience_config_with_empty_regions( "component": "privacy_center", "description": "We take your privacy seriously", "privacy_policy_link_label": "Manage your privacy", - "privacy_policy_url": "example.com/privacy", + "privacy_policy_url": "http://example.com/privacy", "regions": [], "reject_button_label": "No", "save_button_label": "Save", @@ -566,7 +599,7 @@ def test_create_experience_config_with_empty_regions( assert resp["component"] == "privacy_center" assert resp["description"] == "We take your privacy seriously" assert resp["privacy_policy_link_label"] == "Manage your privacy" - assert resp["privacy_policy_url"] == "example.com/privacy" + assert resp["privacy_policy_url"] == "http://example.com/privacy" assert resp["regions"] == [] assert resp["reject_button_label"] == "No" assert resp["save_button_label"] == "Save" @@ -624,7 +657,7 @@ def test_create_experience_config_no_existing_experiences( "description": "We care about your privacy. Opt in and opt out of the data use cases below.", "privacy_preferences_link_label": "Control your privacy", "privacy_policy_link_label": "Control your privacy", - "privacy_policy_url": "example.com/privacy", + "privacy_policy_url": "http://example.com/privacy", "regions": ["us_ny"], "reject_button_label": "Reject all", "save_button_label": "Save", @@ -645,7 +678,7 @@ def test_create_experience_config_no_existing_experiences( ) assert resp["privacy_preferences_link_label"] == "Control your privacy" assert resp["privacy_policy_link_label"] == "Control your privacy" - assert resp["privacy_policy_url"] == "example.com/privacy" + assert resp["privacy_policy_url"] == "http://example.com/privacy" assert resp["regions"] == ["us_ny"] assert resp["reject_button_label"] == "Reject all" assert resp["save_button_label"] == "Save" @@ -667,7 +700,7 @@ def test_create_experience_config_no_existing_experiences( experience_config.privacy_preferences_link_label == "Control your privacy" ) assert experience_config.privacy_policy_link_label == "Control your privacy" - assert experience_config.privacy_policy_url == "example.com/privacy" + assert experience_config.privacy_policy_url == "http://example.com/privacy" assert experience_config.regions == [PrivacyNoticeRegion.us_ny] assert experience_config.reject_button_label == "Reject all" assert experience_config.save_button_label == "Save" @@ -700,7 +733,9 @@ def test_create_experience_config_no_existing_experiences( experience_config_history.privacy_policy_link_label == "Control your privacy" ) - assert experience_config_history.privacy_policy_url == "example.com/privacy" + assert ( + experience_config_history.privacy_policy_url == "http://example.com/privacy" + ) assert experience_config_history.reject_button_label == "Reject all" assert experience_config_history.save_button_label == "Save" assert experience_config_history.title == "Control your privacy" @@ -758,7 +793,7 @@ def test_create_experience_config_existing_experiences( "description": "We care about your privacy. Opt in and opt out of the data use cases below.", "privacy_preferences_link_label": "Control your privacy", "privacy_policy_link_label": "Control your privacy", - "privacy_policy_url": "example.com/privacy", + "privacy_policy_url": "http://example.com/privacy", "regions": ["us_tx"], "reject_button_label": "Reject all", "save_button_label": "Save", @@ -779,7 +814,7 @@ def test_create_experience_config_existing_experiences( ) assert resp["privacy_preferences_link_label"] == "Control your privacy" assert resp["privacy_policy_link_label"] == "Control your privacy" - assert resp["privacy_policy_url"] == "example.com/privacy" + assert resp["privacy_policy_url"] == "http://example.com/privacy" assert resp["regions"] == ["us_tx"] assert resp["reject_button_label"] == "Reject all" assert resp["save_button_label"] == "Save" @@ -957,7 +992,7 @@ def overlay_experience_config(self, db) -> PrivacyExperienceConfig: "disabled": False, "privacy_preferences_link_label": "Manage preferences", "privacy_policy_link_label": "View our privacy policy", - "privacy_policy_url": "example.com/privacy", + "privacy_policy_url": "http://example.com/privacy", "reject_button_label": "Reject all", "save_button_label": "Save", "title": "Control your privacy",
tests/ops/api/v1/endpoints/test_privacy_preference_endpoints.py+1 −1 modified@@ -2073,7 +2073,7 @@ def test_get_historical_preferences( assert response_body["user_geography"] == "us_ca" assert response_body["relevant_systems"] == [system.fides_key] assert response_body["affected_system_status"] == {system.fides_key: "complete"} - assert response_body["url_recorded"] == "example.com/privacy_center" + assert response_body["url_recorded"] == "https://example.com/privacy_center" assert ( response_body["user_agent"] == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/324.42 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/425.24"
tests/ops/models/test_privacy_experience.py+5 −5 modified@@ -47,7 +47,7 @@ def test_create_privacy_experience_config(self, db): "description": "We care about your privacy. Opt in and opt out of the data use cases below.", "privacy_preferences_link_label": "Manage preferences", "privacy_policy_link_label": "View our privacy policy", - "privacy_policy_url": "example.com/privacy", + "privacy_policy_url": "http://example.com/privacy", "reject_button_label": "Reject all", "save_button_label": "Save", "title": "Control your privacy", @@ -66,7 +66,7 @@ def test_create_privacy_experience_config(self, db): assert config.is_default is False assert config.privacy_preferences_link_label == "Manage preferences" assert config.privacy_policy_link_label == "View our privacy policy" - assert config.privacy_policy_url == "example.com/privacy" + assert config.privacy_policy_url == "http://example.com/privacy" assert config.reject_button_label == "Reject all" assert config.save_button_label == "Save" assert config.title == "Control your privacy" @@ -91,7 +91,7 @@ def test_create_privacy_experience_config(self, db): assert history.is_default is False assert history.privacy_preferences_link_label == "Manage preferences" assert history.privacy_policy_link_label == "View our privacy policy" - assert history.privacy_policy_url == "example.com/privacy" + assert history.privacy_policy_url == "http://example.com/privacy" assert history.reject_button_label == "Reject all" assert history.save_button_label == "Save" assert history.title == "Control your privacy" @@ -112,7 +112,7 @@ def test_update_privacy_experience_config(self, db): "description": "We care about your privacy. Opt in and opt out of the data use cases below.", "privacy_preferences_link_label": "Manage preferences", "privacy_policy_link_label": "View our privacy policy", - "privacy_policy_url": "example.com/privacy", + "privacy_policy_url": "http://example.com/privacy", "reject_button_label": "Reject all", "save_button_label": "Save", "title": "Control your privacy", @@ -473,7 +473,7 @@ def test_get_should_show_banner(self, db): "description": "We care about your privacy. Opt in and opt out of the data use cases below.", "privacy_preferences_link_label": "Manage preferences", "privacy_policy_link_label": "View our privacy policy", - "privacy_policy_url": "example.com/privacy", + "privacy_policy_url": "http://example.com/privacy", "reject_button_label": "Reject all", "save_button_label": "Save", "title": "Control your privacy",
tests/ops/models/test_privacy_preference.py+17 −11 modified@@ -136,7 +136,7 @@ def test_create_privacy_preference_for_notice( "secondary_user_ids": {"ga_client_id": "test"}, "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/324.42 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/425.24", "user_geography": "us_ca", - "url_recorded": "example.com/privacy_center", + "url_recorded": "https://example.com/privacy_center", }, check_name=False, ) @@ -194,7 +194,10 @@ def test_create_privacy_preference_for_notice( == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/324.42 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/425.24" ) assert preference_history_record.user_geography == "us_ca" - assert preference_history_record.url_recorded == "example.com/privacy_center" + assert ( + preference_history_record.url_recorded + == "http://example.com/privacy_center" + ) # Assert PrivacyRequest.privacy_preferences relationship assert privacy_request.privacy_preferences == [] @@ -237,7 +240,7 @@ def test_create_privacy_preference_for_notice( "secondary_user_ids": {"ga_client_id": "test"}, "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/324.42 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/425.24", "user_geography": "us_ca", - "url_recorded": "example.com/privacy_center", + "url_recorded": "http://example.com/privacy_center", }, check_name=False, ) @@ -662,7 +665,7 @@ def test_create_history_and_upsert_current_preferences( "secondary_user_ids": {"ga_client_id": "test"}, "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/324.42 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/425.24", "user_geography": "us_ca", - "url_recorded": "example.com/privacy_center", + "url_recorded": "http://example.com/privacy_center", }, check_name=False, ) @@ -758,7 +761,7 @@ def test_consolidate_current_privacy_preferences(self, db, privacy_notice): "secondary_user_ids": {"ga_client_id": "test"}, "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/324.42 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/425.24", "user_geography": "us_ca", - "url_recorded": "example.com/privacy_center", + "url_recorded": "http://example.com/privacy_center", }, check_name=False, ) @@ -818,7 +821,7 @@ def test_consolidate_current_privacy_preferences(self, db, privacy_notice): "secondary_user_ids": {"ga_client_id": "test"}, "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/324.42 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/425.24", "user_geography": "us_ca", - "url_recorded": "example.com/privacy_center", + "url_recorded": "http://example.com/privacy_center", }, check_name=False, ) @@ -860,7 +863,7 @@ def test_consolidate_current_privacy_preferences(self, db, privacy_notice): "secondary_user_ids": {"ga_client_id": "test"}, "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/324.42 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/425.24", "user_geography": "us_ca", - "url_recorded": "example.com/privacy_center", + "url_recorded": "http://example.com/privacy_center", }, check_name=False, ) @@ -1076,7 +1079,7 @@ def test_update_current_privacy_preferences_fides_id_only( "secondary_user_ids": {"ga_client_id": "test"}, "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/324.42 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/425.24", "user_geography": "us_ca", - "url_recorded": "example.com/privacy_center", + "url_recorded": "http://example.com/privacy_center", }, check_name=False, ) @@ -1110,7 +1113,7 @@ def test_update_current_privacy_preferences_fides_id_only( "secondary_user_ids": {"ga_client_id": "test"}, "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/324.42 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/425.24", "user_geography": "us_ca", - "url_recorded": "example.com/privacy_center", + "url_recorded": "http://example.com/privacy_center", }, check_name=False, ) @@ -1867,7 +1870,7 @@ def test_create_served_notice_history_for_notice( "request_origin": "privacy_center", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/324.42 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/425.24", "user_geography": "us_co", - "url_recorded": "example.com/privacy_center", + "url_recorded": "http://example.com/privacy_center", "acknowledge_mode": False, "serving_component": ServingComponent.privacy_center, "privacy_experience_id": privacy_experience_privacy_center.id, @@ -1914,7 +1917,10 @@ def test_create_served_notice_history_for_notice( == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/324.42 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/425.24" ) assert served_notice_history_record.user_geography == "us_co" - assert served_notice_history_record.url_recorded == "example.com/privacy_center" + assert ( + served_notice_history_record.url_recorded + == "http://example.com/privacy_center" + ) # Assert ServedNoticeHistory record upserted last_served_notice = served_notice_history_record.last_served_record
tests/ops/util/test_consent_util.py+17 −6 modified@@ -1,3 +1,4 @@ +"""Test consent utils""" from __future__ import annotations from html import unescape @@ -1051,7 +1052,7 @@ def default_overlay_config_data(self, db): "id": "test_id", "privacy_preferences_link_label": "D", "privacy_policy_link_label": "E's label", - "privacy_policy_url": "F", + "privacy_policy_url": "https://example.com/privacy_policy", "reject_button_label": "G", "save_button_label": "H", "title": "I", @@ -1075,7 +1076,9 @@ def test_create_default_experience_config(self, db, default_overlay_config_data) assert ( experience_config.privacy_policy_link_label == "E's label" ) # Escaped - assert experience_config.privacy_policy_url == "F" + assert ( + experience_config.privacy_policy_url == "https://example.com/privacy_policy" + ) assert experience_config.regions == [] assert experience_config.reject_button_label == "G" assert experience_config.save_button_label == "H" @@ -1101,7 +1104,7 @@ def test_create_default_experience_config(self, db, default_overlay_config_data) assert history.id != "test_id" assert history.privacy_preferences_link_label == "D" assert history.privacy_policy_link_label == "E's label" - assert history.privacy_policy_url == "F" + assert history.privacy_policy_url == "https://example.com/privacy_policy" assert history.reject_button_label == "G" assert history.save_button_label == "H" assert history.title == "I" @@ -1141,7 +1144,9 @@ def test_default_experience_config_data_has_changed( ) assert experience_config is not None - default_overlay_config_data["privacy_policy_url"] = "example.com/privacy_policy" + default_overlay_config_data[ + "privacy_policy_url" + ] = "https://test_example.com/privacy_policy" resp = create_default_experience_config(db, default_overlay_config_data) assert resp is None @@ -1150,15 +1155,21 @@ def test_default_experience_config_data_has_changed( # Data has changed but we didn't update existing config assert experience_config.version == 1.0 - assert experience_config.privacy_policy_url != "example.com/privacy_policy" + assert ( + experience_config.privacy_policy_url + != "https://test_example.com/privacy_policy" + ) assert experience_config.histories.count() == 1 assert experience_config.experience_config_history_id is not None assert experience_config.experience_config_history_id != "test_id" history = experience_config.histories[0] assert history.version == 1.0 - assert experience_config.privacy_policy_url != "example.com/privacy_policy" + assert ( + experience_config.privacy_policy_url + != "https://test_example.com/privacy_policy" + ) history.delete(db) experience_config.delete(db)
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-fgjj-5jmr-gh83ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-46126ghsaADVISORY
- github.com/ethyca/fides/commit/3231d19699f9c895c986f6a967a64d882769c506ghsax_refsource_MISCWEB
- github.com/ethyca/fides/releases/tag/2.22.1ghsax_refsource_MISCWEB
- github.com/ethyca/fides/security/advisories/GHSA-fgjj-5jmr-gh83ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.