CVE-2023-37481: 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 this vulnerability to upload zip files containing malicious SVG bombs (similar to a billion laughs attack), causing resource exhaustion in Admin UI browser tabs and creating a persistent denial of service of the 'new connector' page (`datastore-connection/new`). 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.
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: requirements.txt, 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 18:40:19 +0100
Commit message: Merge pull request from GHSA-3rw2-wfc8-wmj5

* Adding checks for malicious SVGs

* Fixing merge conflicts

---------

Co-authored-by: Dave Quinlan <83430497+daveqnet@users.noreply.github.com>
Diff: diff --git a/requirements.txt b/requirements.txt
index 90ee39b71..8b92504e3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,10 +6,11 @@ boto3==1.26.1
 celery[pytest]==5.2.7
 colorama>=0.4.3
 cryptography==38.0.3
 dask==2022.9.2
 deepdiff==6.3.0
+defusedxml==0.7.1
 expandvars==0.9.0
 fastapi[all]==0.89.1
 fastapi-caching[redis]==0.3.0
 fastapi-pagination[sqlalchemy]~= 0.10.0
 fideslang==1.4.2
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 aab226ada..fe37d50ad 100644
--- a/src/fides/api/service/connectors/saas/connector_registry_service.py
+++ b/src/fides/api/service/connectors/saas/connector_registry_service.py
@@ -38,11 +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.api.util.unsafe_file_util import verify_svg, verify_zip
 from fides.config import CONFIG
 
 
 class ConnectorTemplateLoader(ABC):
     _instance: Optional["ConnectorTemplateLoader"] = None
@@ -212,10 +212,11 @@ class CustomConnectorTemplateLoader(ConnectorTemplateLoader):
                     raise ValidationError(
                         "Multiple files ending with dataset.yml found, only one is allowed."
                     )
             elif info.filename.endswith(".svg"):
                 if not icon_contents:
+                    verify_svg(file_contents)
                     icon_contents = str_to_b64_str(file_contents)
                 else:
                     raise ValidationError(
                         "Multiple svg files found, only one is allowed."
                     )
diff --git a/src/fides/api/util/unsafe_file_util.py b/src/fides/api/util/unsafe_file_util.py
index d8ef9bc62..5c5d99a40 100644
--- a/src/fides/api/util/unsafe_file_util.py
+++ b/src/fides/api/util/unsafe_file_util.py
@@ -1,12 +1,36 @@
 from typing import Optional
 from zipfile import ZipFile
 
+from defusedxml.ElementTree import fromstring
+
+from fides.api.common_exceptions import ValidationError
+
 MAX_FILE_SIZE = 16 * 1024 * 1024  # 16 MB
 CHUNK_SIZE = 1024
 
 
+def verify_svg(contents: str) -> None:
+    """
+    Verifies the provided SVG content.
+
+    This function checks the given SVG content string for potential issues and throws an exception if any are found.
+    It first attempts to parse the SVG content using 'defusedxml.fromstring'. If the parsing is unsuccessful, this
+    will raise an exception, indicating that the SVG content may contain unsafe XML.
+
+    :param contents: The SVG content as a string.
+    :raises ValidationError: If the SVG content contains unsafe XML or 'use xlink'
+    """
+    try:
+        fromstring(contents)
+    except Exception:
+        raise ValidationError("SVG file contains unsafe XML.")
+
+    if "use xlink" in contents:
+        raise ValidationError("SVG files with xlink references are not allowed.")
+
+
 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
diff --git a/tests/ops/util/test_unsafe_file_util.py b/tests/ops/util/test_unsafe_file_util.py
index d452bcf23..3cadda9b0 100644
--- a/tests/ops/util/test_unsafe_file_util.py
+++ b/tests/ops/util/test_unsafe_file_util.py
@@ -1,14 +1,61 @@
 from io import BytesIO
 from zipfile import ZipFile
 
 import pytest
 
-from fides.api.util.unsafe_file_util import verify_zip
+from fides.api.common_exceptions import ValidationError
+from fides.api.util.unsafe_file_util import verify_svg, verify_zip
 from tests.ops.test_helpers.saas_test_utils import create_zip_file
 
 
+class TestVerifySvg:
+    def test_verify_svg(self):
+        verify_svg(
+            """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
+                <circle cx="50" cy="50" r="40"/>
+            </svg>
+            """
+        )
+
+    def test_verify_svg_no_laughing_allowed(self):
+        """Test "billion laughs attack" is prevented"""
+        with pytest.raises(ValidationError) as exc:
+            verify_svg(
+                """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+                <!DOCTYPE svg [
+                <!ENTITY lol "lol">
+                <!ELEMENT lolz (#PCDATA)>
+                <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
+                <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
+                <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
+                <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
+                <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
+                <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
+                <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
+                <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
+                <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
+                ]>
+                <svg>
+                    <lolz>&lol9;</lolz>
+                </svg>
+                """
+            )
+        assert "SVG file contains unsafe XML." in str(exc.value)
+
+    def test_verify_svg_with_xlink(self):
+        with pytest.raises(ValidationError) as exc:
+            verify_svg(
+                """<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100">
+                    <circle id="circle" cx="50" cy="50" r="40"/>
+                    <use xlink:href="#circle"/>
+                </svg>
+                """
+            )
+        assert "SVG files with xlink references are not allowed." in str(exc.value)
+
+
 class TestVerifyZip:
     @pytest.fixture
     def zip_file(self) -> BytesIO:
         return create_zip_file(
             {

Vulnerable code: from fides.api.util.unsafe_file_util import verify_zip
from fides.api.util.unsafe_file_util import verify_zip
Fixed code: defusedxml==0.7.1
from fides.api.util.unsafe_file_util import verify_svg, verify_zip
                    verify_svg(file_contents)
from defusedxml.ElementTree import fromstring

from fides.api.common_exceptions import ValidationError

def verify_svg(contents: str) -> None:
    """
    Verifies the provided SVG content.

    This function checks the given SVG content string for potential issues and throws an exception if any are found.
    It first attempts to parse the SVG content using 'defusedxml.fromstring'. If the parsing is unsuccessful, this
    will raise an exception, indicating that the SVG content may contain unsafe XML.

    :param contents: The SVG content as a string.
    :raises ValidationError: If the SVG content contains unsafe XML or 'use xlink'
    """
    try:
        fromstring(contents)
    except Exception:
        raise ValidationError("SVG file contains unsafe XML.")

    if "use xlink" in contents:
        raise ValidationError("SVG files with xlink references are not allowed.")


from fides.api.common_exceptions import ValidationError
from fides.api.util.unsafe_file_util import verify_svg, verify_zip
class TestVerifySvg:
    def test_verify_svg(self):
        verify_svg(
            """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
                <circle cx="50" cy="50" r="40"/>
            </svg>
            """
        )

    def test_verify_svg_no_laughing_allowed(self):
        """Test "billion laughs attack" is prevented"""
        with pytest.raises(ValidationError) as exc:
            verify_svg(
                """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
                <!DOCTYPE svg [
                <!ENTITY lol "lol">
                <!ELEMENT lolz (#PCDATA)>
                <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
                <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
                <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
                <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
                <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
                <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
                <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
                <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
                <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
                ]>
                <svg>
                    <lolz>&lol9;</lolz>
                </svg>
                """
            )
        assert "SVG file contains unsafe XML." in str(exc.value)

    def test_verify_svg_with_xlink(self):
        with pytest.raises(ValidationError) as exc:
            verify_svg(
                """<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100">
                    <circle id="circle" cx="50" cy="50" r="40"/>
                    <use xlink:href="#circle"/>
                </svg>
                """
            )
        assert "SVG files with xlink references are not allowed." in str(exc.value)