diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 5a4bdcf3d4..ea4f957077 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -30,6 +30,7 @@ All notable changes to the **Prowler API** are documented in this file. ### 🔐 Security - Use `psycopg2.sql` to safely compose DDL in `PostgresEnumMigration`, preventing SQL injection via f-string interpolation [(#10166)](https://github.com/prowler-cloud/prowler/pull/10166) +- Replace stdlib XML parser with `defusedxml` in SAML metadata parsing to prevent XML bomb (billion laughs) DoS attacks [(#10165)](https://github.com/prowler-cloud/prowler/pull/10165) --- diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 7b7ccb9061..78bfec5262 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -1,7 +1,6 @@ import json import logging import re -import xml.etree.ElementTree as ET from datetime import datetime, timedelta, timezone from uuid import UUID, uuid4 @@ -9,6 +8,8 @@ from allauth.socialaccount.models import SocialApp from config.custom_logging import BackendLogger from config.settings.social_login import SOCIALACCOUNT_PROVIDERS from cryptography.fernet import Fernet, InvalidToken +import defusedxml +from defusedxml import ElementTree as ET from django.conf import settings from django.contrib.auth.models import AbstractBaseUser from django.contrib.postgres.fields import ArrayField @@ -2067,6 +2068,8 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel): root = ET.fromstring(self.metadata_xml) except ET.ParseError as e: raise ValidationError({"metadata_xml": f"Invalid XML: {e}"}) + except defusedxml.DefusedXmlException as e: + raise ValidationError({"metadata_xml": f"Unsafe XML content rejected: {e}"}) # Entity ID entity_id = root.attrib.get("entityID") diff --git a/api/src/backend/api/tests/test_models.py b/api/src/backend/api/tests/test_models.py index 13878794b1..b8b7f61dd1 100644 --- a/api/src/backend/api/tests/test_models.py +++ b/api/src/backend/api/tests/test_models.py @@ -243,6 +243,39 @@ class TestSAMLConfigurationModel: assert "Invalid XML" in errors["metadata_xml"][0] assert "not well-formed" in errors["metadata_xml"][0] + def test_xml_bomb_rejected(self, tenants_fixture): + """ + Regression test: a 'billion laughs' XML bomb in the SAML metadata field + must be rejected and not allowed to exhaust server memory / CPU. + + Before the fix, xml.etree.ElementTree was used directly, which does not + protect against entity-expansion attacks. The fix switches to defusedxml + which raises an exception for any XML containing entity definitions. + """ + tenant = tenants_fixture[0] + xml_bomb = ( + "" + "" + " " + " " + " " + "]>" + "" + ) + config = SAMLConfiguration( + email_domain="xmlbomb.com", + metadata_xml=xml_bomb, + tenant=tenant, + ) + + with pytest.raises(ValidationError) as exc_info: + config._parse_metadata() + + errors = exc_info.value.message_dict + assert "metadata_xml" in errors + def test_metadata_missing_sso_fails(self, tenants_fixture): tenant = tenants_fixture[0] xml = """