CVE-2023-37480: Uncontrolled Resource Consumption (CWE-400)
value: Fides is an open-source privacy engineering platform for managing data privacy requests and privacy regulations. The Fides webserver is vulnerable to a type of Denial of Service (DoS) attack. Attackers can exploit a weakness in the connector template upload feature to upload a malicious zip bomb file, resulting in resource exhaustion and service unavailability for all users of the Fides webserver. This vulnerability affects Fides versions `2.11.0` through `2.15.1`. Exploitation is limited to users with elevated privileges with the `CONNECTOR_TEMPLATE_REGISTER` scope, which includes root users and users with the owner role. The vulnerability has been patched in Fides version `2.16.0`. Users are advised to upgrade to this version or later to secure their systems against this threat. There is no known workaround to remediate this vulnerability without upgrading. If an attack occurs, the impact can be mitigated by manually or automatically restarting the affected container.

CWE description: The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
CVSS v3 base score: 4.9
Published: 2023-07-18T19:15Z
Repository: https://github.com/ethyca/fides
Language: Python
Files changed: src/fides/api/service/connectors/saas/connector_registry_service.py, src/fides/api/util/unsafe_file_util.py, tests/ops/util/test_unsafe_file_util.py
Commit date: 2023-07-06 17:46:17 +0100
Commit message: Merge pull request from GHSA-g95c-2jgm-hqc6

Co-authored-by: Dave Quinlan <83430497+daveqnet@users.noreply.github.com>
Diff: diff --git a/src/fides/api/service/connectors/saas/connector_registry_service.py b/src/fides/api/service/connectors/saas/connector_registry_service.py
index e832940d0..aab226ada 100644
--- a/src/fides/api/service/connectors/saas/connector_registry_service.py
+++ b/src/fides/api/service/connectors/saas/connector_registry_service.py
@@ -38,10 +38,11 @@ from fides.api.util.saas_util import (
     load_yaml_as_string,
     replace_config_placeholders,
     replace_dataset_placeholders,
     replace_version,
 )
+from fides.api.util.unsafe_file_util import verify_zip
 from fides.config import CONFIG
 
 
 class ConnectorTemplateLoader(ABC):
     _instance: Optional["ConnectorTemplateLoader"] = None
@@ -179,10 +180,13 @@ class CustomConnectorTemplateLoader(ConnectorTemplateLoader):
         """
         Extracts and validates the contents of a zip file containing a
         custom connector template, registers the template, and saves it to the database.
         """
 
+        # verify the zip file before we use it
+        verify_zip(zip_file)
+
         config_contents = None
         dataset_contents = None
         icon_contents = None
         function_contents = None
 
diff --git a/src/fides/api/util/unsafe_file_util.py b/src/fides/api/util/unsafe_file_util.py
new file mode 100644
index 000000000..d8ef9bc62
--- /dev/null
+++ b/src/fides/api/util/unsafe_file_util.py
@@ -0,0 +1,34 @@
+from typing import Optional
+from zipfile import ZipFile
+
+MAX_FILE_SIZE = 16 * 1024 * 1024  # 16 MB
+CHUNK_SIZE = 1024
+
+
+def verify_zip(zip_file: ZipFile, max_file_size: Optional[int] = None) -> None:
+    """
+    Function to safely verify the contents of zipped files. It prevents potential
+    'zip bomb' attacks by checking the file size of the files in the zip without fully
+    extracting them. If the size of any file in the zip exceeds the specified
+    max_file_size, it raises a ValueError. If the max_file_size is not provided,
+    it uses a default value of 16 MB.
+
+    :param zip_file: A ZipFile object to be verified.
+    :param max_file_size: An optional integer specifying the maximum bytes allowed per file. If not provided, a default value is used.
+    :raises ValueError: If a file in the zip file exceeds the maximum allowed size
+    """
+
+    if max_file_size is None:
+        max_file_size = MAX_FILE_SIZE
+
+    for file_info in zip_file.infolist():
+        file_size = 0
+
+        with zip_file.open(file_info) as file:
+            # wraps the file read in an iterator that stops once no bytes
+            # are returned or the max file size is reached
+            for chunk in iter(lambda: file.read(CHUNK_SIZE), b""):
+                file_size += len(chunk)
+
+                if file_size > max_file_size:
+                    raise ValueError("File size exceeds maximum allowed size")
diff --git a/tests/ops/util/test_unsafe_file_util.py b/tests/ops/util/test_unsafe_file_util.py
new file mode 100644
index 000000000..d452bcf23
--- /dev/null
+++ b/tests/ops/util/test_unsafe_file_util.py
@@ -0,0 +1,26 @@
+from io import BytesIO
+from zipfile import ZipFile
+
+import pytest
+
+from fides.api.util.unsafe_file_util import verify_zip
+from tests.ops.test_helpers.saas_test_utils import create_zip_file
+
+
+class TestVerifyZip:
+    @pytest.fixture
+    def zip_file(self) -> BytesIO:
+        return create_zip_file(
+            {
+                "config.yml": "This file isn't that big, but it will be considered suspicious if the max file size is set too low",
+            }
+        )
+
+    def test_verify_zip(self, zip_file):
+        verify_zip(ZipFile(zip_file))
+
+    def test_verify_zip_with_small_file_size_limit(self, zip_file):
+        """We set the max file size to 1 byte, so the zip file should be rejected."""
+        with pytest.raises(ValueError) as exc:
+            verify_zip(ZipFile(zip_file), 1)
+        assert "File size exceeds maximum allowed size" in str(exc.value)

Fixed code: from fides.api.util.unsafe_file_util import verify_zip
        # verify the zip file before we use it
        verify_zip(zip_file)

from typing import Optional
from zipfile import ZipFile

MAX_FILE_SIZE = 16 * 1024 * 1024  # 16 MB
CHUNK_SIZE = 1024


def verify_zip(zip_file: ZipFile, max_file_size: Optional[int] = None) -> None:
    """
    Function to safely verify the contents of zipped files. It prevents potential
    'zip bomb' attacks by checking the file size of the files in the zip without fully
    extracting them. If the size of any file in the zip exceeds the specified
    max_file_size, it raises a ValueError. If the max_file_size is not provided,
    it uses a default value of 16 MB.

    :param zip_file: A ZipFile object to be verified.
    :param max_file_size: An optional integer specifying the maximum bytes allowed per file. If not provided, a default value is used.
    :raises ValueError: If a file in the zip file exceeds the maximum allowed size
    """

    if max_file_size is None:
        max_file_size = MAX_FILE_SIZE

    for file_info in zip_file.infolist():
        file_size = 0

        with zip_file.open(file_info) as file:
            # wraps the file read in an iterator that stops once no bytes
            # are returned or the max file size is reached
            for chunk in iter(lambda: file.read(CHUNK_SIZE), b""):
                file_size += len(chunk)

                if file_size > max_file_size:
                    raise ValueError("File size exceeds maximum allowed size")
from io import BytesIO
from zipfile import ZipFile

import pytest

from fides.api.util.unsafe_file_util import verify_zip
from tests.ops.test_helpers.saas_test_utils import create_zip_file


class TestVerifyZip:
    @pytest.fixture
    def zip_file(self) -> BytesIO:
        return create_zip_file(
            {
                "config.yml": "This file isn't that big, but it will be considered suspicious if the max file size is set too low",
            }
        )

    def test_verify_zip(self, zip_file):
        verify_zip(ZipFile(zip_file))

    def test_verify_zip_with_small_file_size_limit(self, zip_file):
        """We set the max file size to 1 byte, so the zip file should be rejected."""
        with pytest.raises(ValueError) as exc:
            verify_zip(ZipFile(zip_file), 1)
        assert "File size exceeds maximum allowed size" in str(exc.value)