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 = """