VYPR
High severityNVD Advisory· Published Sep 6, 2023· Updated Sep 26, 2024

Remote Code Execution in Custom Integration Upload in Fides

CVE-2023-41319

Description

Fides is an open-source privacy engineering platform for managing the fulfillment of data privacy requests in a runtime environment, and the enforcement of privacy regulations in code. The Fides webserver API allows custom integrations to be uploaded as a ZIP file. This ZIP file must contain YAML files, but Fides can be configured to also accept the inclusion of custom Python code in it. The custom code is executed in a restricted, sandboxed environment, but the sandbox can be bypassed to execute any arbitrary code. The vulnerability allows the execution of arbitrary code on the target system within the context of the webserver python process owner on the webserver container, which by default is root, and leverage that access to attack underlying infrastructure and integrated systems. This vulnerability affects Fides versions 2.11.0 through 2.19.0. Exploitation is limited to API clients with the CONNECTOR_TEMPLATE_REGISTER authorization scope. In the Fides Admin UI this scope is restricted to highly privileged users, specifically root users and users with the owner role. Exploitation is only possible if the security configuration parameter allow_custom_connector_functions is enabled by the user deploying the Fides webserver container, either in fides.toml or by setting the env var FIDES__SECURITY__ALLOW_CUSTOM_CONNECTOR_FUNCTIONS=True. By default this configuration parameter is disabled. The vulnerability has been patched in Fides version 2.19.0. Users are advised to upgrade to this version or later to secure their systems against this threat. Users unable to upgrade should ensure that allow_custom_connector_functions in fides.toml and the FIDES__SECURITY__ALLOW_CUSTOM_CONNECTOR_FUNCTIONS are both either unset or explicit set to False.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
ethyca-fidesPyPI
>= 2.11.0, < 2.19.02.19.0

Affected products

1

Patches

1
5989b5fa744c

Merge pull request from GHSA-p6p2-qq95-vq5h

https://github.com/ethyca/fidesAdrian GalvanSep 5, 2023via ghsa
9 files changed · +43 414
  • clients/admin-ui/src/features/connector-templates/ConnectorTemplateUploadModal.tsx+1 2 modified
    @@ -122,8 +122,7 @@ const ConnectorTemplateUploadModal: React.FC<RequestModalProps> = ({
               </Box>
               <Text fontSize="sm" mt={4}>
                 An integration template zip file must include a SaaS config and
    -            dataset, but may also contain an icon (.svg) and custom functions
    -            (.py) as optional files.
    +            dataset, but may also contain an icon (.svg) as an optional file.
               </Text>
             </ModalBody>
             <ModalFooter>
    
  • docker-compose.yml+0 1 modified
    @@ -28,7 +28,6 @@ services:
           FIDES__DEV_MODE: "True"
           FIDES__LOGGING__COLORIZE: "True"
           FIDES__USER__ANALYTICS_OPT_OUT: "True"
    -      FIDES__SECURITY__ALLOW_CUSTOM_CONNECTOR_FUNCTIONS: "True"
           FIDES__SECURITY__BASTION_SERVER_HOST: ${FIDES__SECURITY__BASTION_SERVER_HOST-}
           FIDES__SECURITY__BASTION_SERVER_SSH_USERNAME: ${FIDES__SECURITY__BASTION_SERVER_SSH_USERNAME-}
           FIDES__SECURITY__BASTION_SERVER_SSH_PRIVATE_KEY: ${FIDES__SECURITY__BASTION_SERVER_SSH_PRIVATE_KEY-}
    
  • requirements.txt+0 2 modified
    @@ -1,4 +1,3 @@
    -AccessControl==6.0
     alembic==1.8.1
     APScheduler==3.9.1.post1
     asyncpg==0.27.0
    @@ -42,7 +41,6 @@ pymssql==2.2.8
     python-jose[cryptography]==3.3.0
     pyyaml==6.0.1
     redis==3.5.3
    -RestrictedPython==6.0.0
     rich-click==1.6.1
     sendgrid==6.9.7
     slowapi==0.1.8
    
  • src/fides/api/schemas/saas/connector_template.py+1 2 modified
    @@ -13,13 +13,12 @@
     class ConnectorTemplate(BaseModel):
         """
         A collection of artifacts that make up a complete
    -    SaaS connector (SaaS config, dataset, icon, functions, etc.)
    +    SaaS connector (SaaS config, dataset, icon, etc.)
         """
     
         config: str
         dataset: str
         icon: Optional[str]
    -    functions: Optional[str]
         human_readable: str
     
         @validator("config")
    
  • src/fides/api/service/connectors/saas/connector_registry_service.py+4 104 modified
    @@ -1,22 +1,17 @@
     # pylint: disable=protected-access
     import os
     from abc import ABC, abstractmethod
    -from ast import AST, AnnAssign
    -from operator import getitem
    -from typing import Any, Dict, Iterable, List, Optional, Tuple, Type
    +from typing import Dict, Iterable, List, Optional, Type
     from zipfile import ZipFile
     
    -from AccessControl.ZopeGuards import safe_builtins
     from fideslang.models import Dataset
     from loguru import logger
     from packaging.version import Version
     from packaging.version import parse as parse_version
    -from RestrictedPython import compile_restricted
    -from RestrictedPython.transformer import RestrictingNodeTransformer
     from sqlalchemy.orm import Session
     
     from fides.api.api.deps import get_api_session
    -from fides.api.common_exceptions import FidesopsException, ValidationError
    +from fides.api.common_exceptions import ValidationError
     from fides.api.cryptography.cryptographic_util import str_to_b64_str
     from fides.api.models.connectionconfig import (
         AccessLevel,
    @@ -41,7 +36,6 @@
         replace_version,
     )
     from fides.api.util.unsafe_file_util import verify_svg, verify_zip
    -from fides.config import CONFIG
     
     
     class ConnectorTemplateLoader(ABC):
    @@ -96,7 +90,6 @@ def _load_connector_templates(self) -> None:
                                 f"data/saas/dataset/{connector_type}_dataset.yml"
                             ),
                             icon=icon,
    -                        functions=None,
                             human_readable=human_readable,
                         )
                     except Exception:
    @@ -151,24 +144,16 @@ def _register_template(
             template: CustomConnectorTemplate,
         ) -> None:
             """
    -        Registers a custom connector template by converting it to a ConnectorTemplate,
    -        registering any custom functions, and adding it to the loader's template dictionary.
    +        Registers a custom connector template by converting it to a ConnectorTemplate
    +        and adding it to the loader's template dictionary.
             """
             connector_template = ConnectorTemplate(
                 config=template.config,
                 dataset=template.dataset,
                 icon=template.icon,
    -            functions=template.functions,
                 human_readable=template.name,
             )
     
    -        # register custom functions if available
    -        if template.functions:
    -            register_custom_functions(template.functions)
    -            logger.info(
    -                f"Loaded functions from the custom connector template '{template.key}'"
    -            )
    -
             # register the template in the loader's template dictionary
             CustomConnectorTemplateLoader.get_connector_templates()[
                 template.key
    @@ -220,13 +205,6 @@ def save_template(cls, db: Session, zip_file: ZipFile) -> None:
                         raise ValidationError(
                             "Multiple svg files found, only one is allowed."
                         )
    -            elif info.filename.endswith(".py"):
    -                if not function_contents:
    -                    function_contents = file_contents
    -                else:
    -                    raise ValidationError(
    -                        "Multiple Python (.py) files found, only one is allowed."
    -                    )
     
             if not config_contents:
                 raise ValidationError("Zip file does not contain a config.yml file.")
    @@ -266,7 +244,6 @@ def save_template(cls, db: Session, zip_file: ZipFile) -> None:
                 config=config_contents,
                 dataset=dataset_contents,
                 icon=icon_contents,
    -            functions=function_contents,
                 replaceable=replaceable,
             )
     
    @@ -447,80 +424,3 @@ def update_saas_instance(
         connection_config.update_saas_config(db, SaaSConfig(**config_from_template))
     
         upsert_dataset_config_from_template(db, connection_config, template, template_vals)
    -
    -
    -def register_custom_functions(script: str) -> None:
    -    """
    -    Registers custom functions by executing the given script in a restricted environment.
    -
    -    The script is compiled and executed with RestrictedPython, which is designed to reduce
    -    the risk of executing untrusted code. It provides a set of safe builtins to prevent
    -    malicious or unintended behavior.
    -
    -    Args:
    -        script (str): The Python script containing the custom functions to be registered.
    -
    -    Raises:
    -        FidesopsException: If allow_custom_connector_functions is disabled.
    -        SyntaxError: If the script contains a syntax error or uses restricted language features.
    -        Exception: If an exception occurs during the execution of the script.
    -    """
    -
    -    if CONFIG.security.allow_custom_connector_functions:
    -        restricted_code = compile_restricted(
    -            script, "<string>", "exec", policy=CustomRestrictingNodeTransformer
    -        )
    -        safe_builtins["__import__"] = custom_guarded_import
    -        safe_builtins["_getitem_"] = getitem
    -        safe_builtins["staticmethod"] = staticmethod
    -
    -        # pylint: disable=exec-used
    -        exec(
    -            restricted_code,
    -            {
    -                "__metaclass__": type,
    -                "__name__": "restricted_module",
    -                "__builtins__": safe_builtins,
    -            },
    -        )
    -    else:
    -        raise FidesopsException(
    -            message="The import of connector templates with custom functions is disabled by the 'security.allow_custom_connector_functions' setting."
    -        )
    -
    -
    -class CustomRestrictingNodeTransformer(RestrictingNodeTransformer):
    -    """
    -    Custom node transformer class that extends RestrictedPython's RestrictingNodeTransformer
    -    to allow the use of type annotations (AnnAssign) in restricted code.
    -    """
    -
    -    def visit_AnnAssign(self, node: AnnAssign) -> AST:
    -        return self.node_contents_visit(node)
    -
    -
    -def custom_guarded_import(
    -    name: str,
    -    _globals: Optional[dict] = None,
    -    _locals: Optional[dict] = None,
    -    fromlist: Optional[Tuple[str, ...]] = None,
    -    level: int = 0,
    -) -> Any:
    -    """
    -    A custom import function that prevents the import of certain potentially unsafe modules.
    -    """
    -    if name in [
    -        "os",
    -        "sys",
    -        "subprocess",
    -        "shutil",
    -        "socket",
    -        "importlib",
    -        "tempfile",
    -        "glob",
    -    ]:
    -        # raising SyntaxError to be consistent with exceptions thrown from other guarded functions
    -        raise SyntaxError(f"Import of '{name}' module is not allowed.")
    -    if fromlist is None:
    -        fromlist = ()
    -    return __import__(name, _globals, _locals, fromlist, level)
    
  • src/fides/config/security_settings.py+0 4 modified
    @@ -118,10 +118,6 @@ class SecuritySettings(FidesSettings):
             default=432000,
             description="The number of seconds that a pre-signed download URL when using S3 storage will be valid. The default is equal to 5 days.",
         )
    -    allow_custom_connector_functions: Optional[bool] = Field(
    -        default=False,
    -        description="Enables or disables the ability to import connector templates with custom functions. When enabled, custom functions which will be loaded in a restricted environment to minimize security risks.",
    -    )
         enable_audit_log_resource_middleware: Optional[bool] = Field(
             default=False,
             description="Either enables the collection of audit log resource data or bypasses the middleware",
    
  • tests/ops/api/v1/endpoints/test_saas_config_endpoints.py+1 115 modified
    @@ -468,14 +468,12 @@ def complete_connector_template(
             self,
             planet_express_config,
             planet_express_dataset,
    -        planet_express_functions,
             planet_express_icon,
         ):
             return create_zip_file(
                 {
                     "config.yml": planet_express_config,
                     "dataset.yml": planet_express_dataset,
    -                "functions.py": planet_express_functions,
                     "icon.svg": planet_express_icon,
                 }
             )
    @@ -484,13 +482,11 @@ def complete_connector_template(
         def connector_template_missing_config(
             self,
             planet_express_dataset,
    -        planet_express_functions,
             planet_express_icon,
         ):
             return create_zip_file(
                 {
                     "dataset.yml": planet_express_dataset,
    -                "functions.py": planet_express_functions,
                     "icon.svg": planet_express_icon,
                 }
             )
    @@ -499,14 +495,12 @@ def connector_template_missing_config(
         def connector_template_wrong_contents_config(
             self,
             planet_express_dataset,
    -        planet_express_functions,
             planet_express_icon,
         ):
             return create_zip_file(
                 {
                     "config.yml": "planet_express_config",
                     "dataset.yml": planet_express_dataset,
    -                "functions.py": planet_express_functions,
                     "icon.svg": planet_express_icon,
                 }
             )
    @@ -516,14 +510,12 @@ def connector_template_invalid_config(
             self,
             planet_express_invalid_config,
             planet_express_dataset,
    -        planet_express_functions,
             planet_express_icon,
         ):
             return create_zip_file(
                 {
                     "config.yml": planet_express_invalid_config,
                     "dataset.yml": planet_express_dataset,
    -                "functions.py": planet_express_functions,
                     "icon.svg": planet_express_icon,
                 }
             )
    @@ -532,13 +524,11 @@ def connector_template_invalid_config(
         def connector_template_missing_dataset(
             self,
             planet_express_config,
    -        planet_express_functions,
             planet_express_icon,
         ):
             return create_zip_file(
                 {
                     "config.yml": planet_express_config,
    -                "functions.py": planet_express_functions,
                     "icon.svg": planet_express_icon,
                 }
             )
    @@ -547,14 +537,12 @@ def connector_template_missing_dataset(
         def connector_template_wrong_contents_dataset(
             self,
             planet_express_config,
    -        planet_express_functions,
             planet_express_icon,
         ):
             return create_zip_file(
                 {
                     "config.yml": planet_express_config,
                     "dataset.yml": "planet_express_dataset",
    -                "functions.py": planet_express_functions,
                     "icon.svg": planet_express_icon,
                 }
             )
    @@ -564,29 +552,12 @@ def connector_template_invalid_dataset(
             self,
             planet_express_config,
             planet_express_invalid_dataset,
    -        planet_express_functions,
             planet_express_icon,
         ):
             return create_zip_file(
                 {
                     "config.yml": planet_express_config,
                     "dataset.yml": planet_express_invalid_dataset,
    -                "functions.py": planet_express_functions,
    -                "icon.svg": planet_express_icon,
    -            }
    -        )
    -
    -    @pytest.fixture
    -    def connector_template_no_functions(
    -        self,
    -        planet_express_config,
    -        planet_express_dataset,
    -        planet_express_icon,
    -    ):
    -        return create_zip_file(
    -            {
    -                "config.yml": planet_express_config,
    -                "dataset.yml": planet_express_dataset,
                     "icon.svg": planet_express_icon,
                 }
             )
    @@ -596,13 +567,11 @@ def connector_template_no_icon(
             self,
             planet_express_config,
             planet_express_dataset,
    -        planet_express_functions,
         ):
             return create_zip_file(
                 {
                     "config.yml": planet_express_config,
                     "dataset.yml": planet_express_dataset,
    -                "functions.py": planet_express_functions,
                 }
             )
     
    @@ -611,15 +580,13 @@ def connector_template_duplicate_configs(
             self,
             planet_express_config,
             planet_express_dataset,
    -        planet_express_functions,
             planet_express_icon,
         ):
             return create_zip_file(
                 {
                     "1_config.yml": planet_express_config,
                     "2_config.yml": planet_express_config,
                     "dataset.yml": planet_express_dataset,
    -                "functions.py": planet_express_functions,
                     "icon.svg": planet_express_icon,
                 }
             )
    @@ -629,33 +596,13 @@ def connector_template_duplicate_datasets(
             self,
             planet_express_config,
             planet_express_dataset,
    -        planet_express_functions,
             planet_express_icon,
         ):
             return create_zip_file(
                 {
                     "config.yml": planet_express_config,
                     "1_dataset.yml": planet_express_dataset,
                     "2_dataset.yml": planet_express_dataset,
    -                "functions.py": planet_express_functions,
    -                "icon.svg": planet_express_icon,
    -            }
    -        )
    -
    -    @pytest.fixture
    -    def connector_template_duplicate_functions(
    -        self,
    -        planet_express_config,
    -        planet_express_dataset,
    -        planet_express_functions,
    -        planet_express_icon,
    -    ):
    -        return create_zip_file(
    -            {
    -                "config.yml": planet_express_config,
    -                "dataset.yml": planet_express_dataset,
    -                "1_functions.py": planet_express_functions,
    -                "2_functions.py": planet_express_functions,
                     "icon.svg": planet_express_icon,
                 }
             )
    @@ -665,14 +612,12 @@ def connector_template_duplicate_icons(
             self,
             planet_express_config,
             planet_express_dataset,
    -        planet_express_functions,
             planet_express_icon,
         ):
             return create_zip_file(
                 {
                     "config.yml": planet_express_config,
                     "dataset.yml": planet_express_dataset,
    -                "functions.py": planet_express_functions,
                     "1_icon.svg": planet_express_icon,
                     "2_icon.svg": planet_express_icon,
                 }
    @@ -685,7 +630,6 @@ def test_register_connector_template_wrong_scope(
             generate_auth_header,
             complete_connector_template,
         ):
    -        CONFIG.security.allow_custom_connector_functions = True
             auth_header = generate_auth_header(scopes=[CLIENT_READ])
             response = api_client.post(
                 register_connector_template_url,
    @@ -746,11 +690,6 @@ def test_register_connector_template_wrong_scope(
                         "detail": "1 validation error for Dataset\ncollections -> 0 -> name\n  field required (type=value_error.missing)"
                     },
                 ),
    -            (
    -                "connector_template_no_functions",
    -                200,
    -                {"message": "Connector template successfully registered."},
    -            ),
                 (
                     "connector_template_no_icon",
                     200,
    @@ -770,66 +709,14 @@ def test_register_connector_template_wrong_scope(
                         "detail": "Multiple files ending with dataset.yml found, only one is allowed."
                     },
                 ),
    -            (
    -                "connector_template_duplicate_functions",
    -                400,
    -                {"detail": "Multiple Python (.py) files found, only one is allowed."},
    -            ),
                 (
                     "connector_template_duplicate_icons",
                     400,
                     {"detail": "Multiple svg files found, only one is allowed."},
                 ),
             ],
         )
    -    @mock.patch(
    -        "fides.api.service.connectors.saas.connector_registry_service.register_custom_functions"
    -    )  # prevent functions from being registered to avoid test conflicts
    -    def test_register_connector_template_allow_custom_connector_functions(
    -        self,
    -        mock_register_custom_functions: MagicMock,
    -        api_client: TestClient,
    -        register_connector_template_url,
    -        generate_auth_header,
    -        zip_file,
    -        status_code,
    -        details,
    -        request,
    -    ):
    -        CONFIG.security.allow_custom_connector_functions = True
    -        auth_header = generate_auth_header(scopes=[CONNECTOR_TEMPLATE_REGISTER])
    -        response = api_client.post(
    -            register_connector_template_url,
    -            headers=auth_header,
    -            files={
    -                "file": (
    -                    "template.zip",
    -                    request.getfixturevalue(zip_file).read(),
    -                    "application/zip",
    -                )
    -            },
    -        )
    -        assert response.status_code == status_code
    -        assert response.json() == details
    -
    -    @pytest.mark.parametrize(
    -        "zip_file, status_code, details",
    -        [
    -            (
    -                "complete_connector_template",
    -                400,
    -                {
    -                    "detail": "The import of connector templates with custom functions is disabled by the 'security.allow_custom_connector_functions' setting."
    -                },
    -            ),
    -            (
    -                "connector_template_no_functions",
    -                200,
    -                {"message": "Connector template successfully registered."},
    -            ),
    -        ],
    -    )
    -    def test_register_connector_template_disallow_custom_connector_functions(
    +    def test_register_connector_template(
             self,
             api_client: TestClient,
             register_connector_template_url,
    @@ -839,7 +726,6 @@ def test_register_connector_template_disallow_custom_connector_functions(
             details,
             request,
         ):
    -        CONFIG.security.allow_custom_connector_functions = False
             auth_header = generate_auth_header(scopes=[CONNECTOR_TEMPLATE_REGISTER])
             response = api_client.post(
                 register_connector_template_url,
    
  • tests/ops/models/test_custom_connector_template.py+0 3 modified
    @@ -12,15 +12,13 @@ def test_create_custom_connector_template(
             planet_express_config,
             planet_express_dataset,
             planet_express_icon,
    -        planet_express_functions,
         ) -> None:
             template = CustomConnectorTemplate(
                 key="planet_express",
                 name="Planet Express",
                 config=planet_express_config,
                 dataset=planet_express_dataset,
                 icon=planet_express_icon,
    -            functions=planet_express_functions,
             )
             template.save(db=db)
     
    @@ -36,4 +34,3 @@ def test_create_custom_connector_template(
             assert custom_connector.config == planet_express_config
             assert custom_connector.dataset == planet_express_dataset
             assert custom_connector.icon == planet_express_icon
    -        assert custom_connector.functions == planet_express_functions
    
  • tests/ops/service/connectors/test_connector_template_loaders.py+36 181 modified
    @@ -16,7 +16,6 @@
         ConnectorRegistry,
         CustomConnectorTemplateLoader,
         FileConnectorTemplateLoader,
    -    register_custom_functions,
     )
     from fides.api.service.saas_request.saas_request_override_factory import (
         SaaSRequestOverrideFactory,
    @@ -113,15 +112,13 @@ def replaceable_planet_express_zip(
             self,
             replaceable_planet_express_config,
             planet_express_dataset,
    -        planet_express_functions,
             planet_express_icon,
         ) -> BytesIO:
             return create_zip_file(
                 {
                     "config.yml": replaceable_planet_express_config,
                     "dataset.yml": planet_express_dataset,
                     "icon.svg": planet_express_icon,
    -                "functions.py": planet_express_functions,
                 }
             )
     
    @@ -135,8 +132,6 @@ def non_replaceable_zendesk_zip(self, zendesk_config, zendesk_dataset) -> BytesI
             )
     
         def test_custom_connector_template_loader_no_templates(self):
    -        CONFIG.security.allow_custom_connector_functions = True
    -
             connector_templates = CustomConnectorTemplateLoader.get_connector_templates()
             assert connector_templates == {}
     
    @@ -148,143 +143,20 @@ def test_custom_connector_template_loader_invalid_template(
             mock_all: MagicMock,
             planet_express_dataset,
             planet_express_icon,
    -        planet_express_functions,
         ):
    -        CONFIG.security.allow_custom_connector_functions = True
    -
             mock_all.return_value = [
                 CustomConnectorTemplate(
                     key="planet_express",
                     name="Planet Express",
                     config="planet_express_config",
                     dataset=planet_express_dataset,
                     icon=planet_express_icon,
    -                functions=planet_express_functions,
    -            )
    -        ]
    -
    -        # verify the custom functions aren't loaded if the template is invalid
    -        connector_templates = CustomConnectorTemplateLoader.get_connector_templates()
    -        assert connector_templates == {}
    -
    -        with pytest.raises(NoSuchSaaSRequestOverrideException):
    -            SaaSRequestOverrideFactory.get_override(
    -                "planet_express_user_access", SaaSRequestType.READ
    -            )
    -
    -        # assert the strategy was not registered
    -        authentication_strategies = AuthenticationStrategy.get_strategies()
    -        assert "planet_express" not in [
    -            strategy.name for strategy in authentication_strategies
    -        ]
    -
    -    @mock.patch(
    -        "fides.api.models.custom_connector_template.CustomConnectorTemplate.all"
    -    )
    -    def test_custom_connector_template_loader_invalid_functions(
    -        self,
    -        mock_all: MagicMock,
    -        planet_express_config,
    -        planet_express_dataset,
    -        planet_express_icon,
    -    ):
    -        CONFIG.security.allow_custom_connector_functions = True
    -
    -        # save custom connector template to the database
    -        mock_all.return_value = [
    -            CustomConnectorTemplate(
    -                key="planet_express",
    -                name="Planet Express",
    -                config=planet_express_config,
    -                dataset=planet_express_dataset,
    -                icon=planet_express_icon,
    -                functions="planet_express_functions",
    -            )
    -        ]
    -
    -        # verify nothing is loaded if the custom functions fail to load
    -        connector_templates = CustomConnectorTemplateLoader.get_connector_templates()
    -        assert connector_templates == {}
    -
    -    @mock.patch(
    -        "fides.api.models.custom_connector_template.CustomConnectorTemplate.all"
    -    )
    -    def test_custom_connector_template_loader_custom_connector_functions_disabled(
    -        self,
    -        mock_all: MagicMock,
    -        planet_express_config,
    -        planet_express_dataset,
    -        planet_express_icon,
    -        planet_express_functions,
    -    ):
    -        CONFIG.security.allow_custom_connector_functions = False
    -
    -        mock_all.return_value = [
    -            CustomConnectorTemplate(
    -                key="planet_express",
    -                name="Planet Express",
    -                config=planet_express_config,
    -                dataset=planet_express_dataset,
    -                icon=planet_express_icon,
    -                functions=planet_express_functions,
                 )
             ]
     
    -        # load custom connector templates from the database
             connector_templates = CustomConnectorTemplateLoader.get_connector_templates()
             assert connector_templates == {}
     
    -        with pytest.raises(NoSuchSaaSRequestOverrideException):
    -            SaaSRequestOverrideFactory.get_override(
    -                "planet_express_user_access", SaaSRequestType.READ
    -            )
    -
    -        # assert the strategy was not registered
    -        authentication_strategies = AuthenticationStrategy.get_strategies()
    -        assert "planet_express" not in [
    -            strategy.name for strategy in authentication_strategies
    -        ]
    -
    -    @mock.patch(
    -        "fides.api.models.custom_connector_template.CustomConnectorTemplate.all"
    -    )
    -    def test_custom_connector_template_loader_custom_connector_functions_disabled_custom_functions(
    -        self,
    -        mock_all: MagicMock,
    -        planet_express_config,
    -        planet_express_dataset,
    -        planet_express_icon,
    -    ):
    -        """
    -        A connector template with no custom functions should still be loaded
    -        even if allow_custom_connector_functions is set to false
    -        """
    -
    -        CONFIG.security.allow_custom_connector_functions = False
    -
    -        # save custom connector template to the database
    -        mock_all.return_value = [
    -            CustomConnectorTemplate(
    -                key="planet_express",
    -                name="Planet Express",
    -                config=planet_express_config,
    -                dataset=planet_express_dataset,
    -                icon=planet_express_icon,
    -                functions=None,
    -            )
    -        ]
    -
    -        # load custom connector templates from the database
    -        connector_templates = CustomConnectorTemplateLoader.get_connector_templates()
    -        assert connector_templates == {
    -            "planet_express": ConnectorTemplate(
    -                config=planet_express_config,
    -                dataset=planet_express_dataset,
    -                icon=planet_express_icon,
    -                human_readable="Planet Express",
    -            )
    -        }
    -
         @mock.patch(
             "fides.api.models.custom_connector_template.CustomConnectorTemplate.all"
         )
    @@ -294,18 +166,14 @@ def test_custom_connector_template_loader(
             planet_express_config,
             planet_express_dataset,
             planet_express_icon,
    -        planet_express_functions,
         ):
    -        CONFIG.security.allow_custom_connector_functions = True
    -
             mock_all.return_value = [
                 CustomConnectorTemplate(
                     key="planet_express",
                     name="Planet Express",
                     config=planet_express_config,
                     dataset=planet_express_dataset,
                     icon=planet_express_icon,
    -                functions=planet_express_functions,
                 )
             ]
     
    @@ -318,22 +186,10 @@ def test_custom_connector_template_loader(
                     config=planet_express_config,
                     dataset=planet_express_dataset,
                     icon=planet_express_icon,
    -                functions=planet_express_functions,
                     human_readable="Planet Express",
                 )
             }
     
    -        # assert the request override was registered
    -        SaaSRequestOverrideFactory.get_override(
    -            "planet_express_user_access", SaaSRequestType.READ
    -        )
    -
    -        # assert the strategy was registered
    -        authentication_strategies = AuthenticationStrategy.get_strategies()
    -        assert "planet_express" in [
    -            strategy.name for strategy in authentication_strategies
    -        ]
    -
         @mock.patch(
             "fides.api.models.custom_connector_template.CustomConnectorTemplate.all"
         )
    @@ -343,18 +199,14 @@ def test_loaders_have_separate_instances(
             planet_express_config,
             planet_express_dataset,
             planet_express_icon,
    -        planet_express_functions,
         ):
    -        CONFIG.security.allow_custom_connector_functions = True
    -
             mock_all.return_value = [
                 CustomConnectorTemplate(
                     key="planet_express",
                     name="Planet Express",
                     config=planet_express_config,
                     dataset=planet_express_dataset,
                     icon=planet_express_icon,
    -                functions=planet_express_functions,
                 )
             ]
     
    @@ -375,7 +227,6 @@ def test_custom_connector_save_template(
             planet_express_config,
             planet_express_dataset,
             planet_express_icon,
    -        planet_express_functions,
         ):
             db = MagicMock()
     
    @@ -386,7 +237,6 @@ def test_custom_connector_save_template(
                         {
                             "config.yml": planet_express_config,
                             "dataset.yml": planet_express_dataset,
    -                        "functions.py": planet_express_functions,
                             "icon.svg": planet_express_icon,
                         }
                     )
    @@ -401,37 +251,55 @@ def test_custom_connector_save_template(
                         {
                             "config.yml": planet_express_config,
                             "dataset.yml": planet_express_dataset,
    -                        "functions.py": planet_express_functions,
                             "icon.svg": planet_express_icon,
                         }
                     )
                 ),
             )
             assert mock_create_or_update.call_count == 2
     
    -    def test_custom_connector_template_loader_disallowed_modules(
    +    @mock.patch(
    +        "fides.api.models.custom_connector_template.CustomConnectorTemplate.create_or_update"
    +    )
    +    def test_custom_connector_save_template_with_functions(
             self,
    +        mock_create_or_update: MagicMock,
             planet_express_config,
             planet_express_dataset,
    +        planet_express_functions,
             planet_express_icon,
         ):
    -        CONFIG.security.allow_custom_connector_functions = True
    -
    -        with pytest.raises(SyntaxError) as exc:
    -            CustomConnectorTemplateLoader.save_template(
    -                MagicMock(),
    -                ZipFile(
    -                    create_zip_file(
    -                        {
    -                            "config.yml": planet_express_config,
    -                            "dataset.yml": planet_express_dataset,
    -                            "functions.py": "import os",
    -                            "icon.svg": planet_express_icon,
    -                        }
    -                    )
    -                ),
    +        db = MagicMock()
    +
    +        CustomConnectorTemplateLoader.save_template(
    +            db,
    +            ZipFile(
    +                create_zip_file(
    +                    {
    +                        "config.yml": planet_express_config,
    +                        "dataset.yml": planet_express_dataset,
    +                        "functions.py": planet_express_functions,
    +                        "icon.svg": planet_express_icon,
    +                    }
    +                )
    +            ),
    +        )
    +
    +        # assert the request override was ignored
    +        with pytest.raises(NoSuchSaaSRequestOverrideException) as exc:
    +            SaaSRequestOverrideFactory.get_override(
    +                "planet_express_user_access", SaaSRequestType.UPDATE
                 )
    -        assert "Import of 'os' module is not allowed." == str(exc.value)
    +        assert (
    +            f"Custom SaaS override 'planet_express_user_access' does not exist."
    +            in str(exc.value)
    +        )
    +
    +        # assert the strategy was ignored
    +        authentication_strategies = AuthenticationStrategy.get_strategies()
    +        assert "planet_express" not in [
    +            strategy.name for strategy in authentication_strategies
    +        ]
     
         @mock.patch(
             "fides.api.models.custom_connector_template.CustomConnectorTemplate.delete"
    @@ -614,16 +482,3 @@ def test_non_replaceable_template(
             config_contents = mock_create_or_update.call_args.kwargs["data"]["config"]
             custom_config = load_config_from_string(config_contents)
             assert custom_config["version"] == "0.0.0"
    -
    -
    -class TestRegisterCustomFunctions:
    -    def test_function_loader(self):
    -        """Verify that all override implementations can be loaded by RestrictedPython"""
    -
    -        overrides_path = "src/fides/api/service/saas_request/override_implementations"
    -
    -        for filename in os.listdir(overrides_path):
    -            if filename.endswith(".py") and filename != "__init__.py":
    -                file_path = os.path.join(overrides_path, filename)
    -                with open(file_path, "r") as file:
    -                    register_custom_functions(file.read())
    

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

4

News mentions

0

No linked articles in our index yet.