Просмотр исходного кода

Fix Bandit B314: Replace xml.etree with defusedxml

Security scan (Bandit) identified vulnerable XML parsing in 3MF file
processing. The standard xml.etree.ElementTree is vulnerable to XXE
(XML External Entity) attacks.

Changes:
- Add defusedxml>=0.7.0 to requirements.txt
- Replace all xml.etree.ElementTree imports with defusedxml.ElementTree
  in production code (6 files)

Affected files:
- backend/app/services/archive.py
- backend/app/services/print_scheduler.py
- backend/app/api/routes/print_queue.py
- backend/app/api/routes/library.py
- backend/app/api/routes/printers.py
- backend/app/api/routes/archives.py

Test files intentionally left unchanged (test XML is trusted).
maziggy 3 месяцев назад
Родитель
Сommit
4b3b615a2c

+ 6 - 4
backend/app/api/routes/archives.py

@@ -1833,7 +1833,8 @@ async def get_archive_capabilities(
 ):
 ):
     """Check what viewing capabilities are available for this 3MF file."""
     """Check what viewing capabilities are available for this 3MF file."""
     import json
     import json
-    import xml.etree.ElementTree as ET
+
+    import defusedxml.ElementTree as ET
 
 
     service = ArchiveService(db)
     service = ArchiveService(db)
     archive = await service.get_archive(archive_id)
     archive = await service.get_archive(archive_id)
@@ -2113,7 +2114,7 @@ async def get_plate_preview(
             plate_num = 1
             plate_num = 1
             if "Metadata/slice_info.config" in names:
             if "Metadata/slice_info.config" in names:
                 try:
                 try:
-                    import xml.etree.ElementTree as ET
+                    import defusedxml.ElementTree as ET
 
 
                     slice_content = zf.read("Metadata/slice_info.config").decode("utf-8")
                     slice_content = zf.read("Metadata/slice_info.config").decode("utf-8")
                     root = ET.fromstring(slice_content)
                     root = ET.fromstring(slice_content)
@@ -2254,7 +2255,8 @@ async def get_archive_plates(
     """
     """
     import json
     import json
     import re
     import re
-    import xml.etree.ElementTree as ET
+
+    import defusedxml.ElementTree as ET
 
 
     service = ArchiveService(db)
     service = ArchiveService(db)
     archive = await service.get_archive(archive_id)
     archive = await service.get_archive(archive_id)
@@ -2560,7 +2562,7 @@ async def get_filament_requirements(
         archive_id: The archive ID
         archive_id: The archive ID
         plate_id: Optional plate index to filter filaments for (for multi-plate files)
         plate_id: Optional plate index to filter filaments for (for multi-plate files)
     """
     """
-    import xml.etree.ElementTree as ET
+    import defusedxml.ElementTree as ET
 
 
     service = ArchiveService(db)
     service = ArchiveService(db)
     archive = await service.get_archive(archive_id)
     archive = await service.get_archive(archive_id)

+ 4 - 2
backend/app/api/routes/library.py

@@ -1312,9 +1312,10 @@ async def get_library_file_plates(
     """
     """
     import json
     import json
     import re
     import re
-    import xml.etree.ElementTree as ET
     import zipfile
     import zipfile
 
 
+    import defusedxml.ElementTree as ET
+
     # Get the library file
     # Get the library file
     result = await db.execute(select(LibraryFile).where(LibraryFile.id == file_id))
     result = await db.execute(select(LibraryFile).where(LibraryFile.id == file_id))
     lib_file = result.scalar_one_or_none()
     lib_file = result.scalar_one_or_none()
@@ -1613,9 +1614,10 @@ async def get_library_file_filament_requirements(
         file_id: The library file ID
         file_id: The library file ID
         plate_id: Optional plate index to get filaments for a specific plate
         plate_id: Optional plate index to get filaments for a specific plate
     """
     """
-    import xml.etree.ElementTree as ET
     import zipfile
     import zipfile
 
 
+    import defusedxml.ElementTree as ET
+
     # Get the library file
     # Get the library file
     result = await db.execute(select(LibraryFile).where(LibraryFile.id == file_id))
     result = await db.execute(select(LibraryFile).where(LibraryFile.id == file_id))
     lib_file = result.scalar_one_or_none()
     lib_file = result.scalar_one_or_none()

+ 1 - 1
backend/app/api/routes/print_queue.py

@@ -2,11 +2,11 @@
 
 
 import json
 import json
 import logging
 import logging
-import xml.etree.ElementTree as ET
 import zipfile
 import zipfile
 from datetime import datetime
 from datetime import datetime
 from pathlib import Path
 from pathlib import Path
 
 
+import defusedxml.ElementTree as ET
 from fastapi import APIRouter, Depends, HTTPException, Query
 from fastapi import APIRouter, Depends, HTTPException, Query
 from sqlalchemy import func, select
 from sqlalchemy import func, select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.ext.asyncio import AsyncSession

+ 2 - 1
backend/app/api/routes/printers.py

@@ -852,9 +852,10 @@ async def get_printer_file_plates(
     """Get available plates from a multi-plate 3MF file stored on a printer."""
     """Get available plates from a multi-plate 3MF file stored on a printer."""
     import io
     import io
     import json
     import json
-    import xml.etree.ElementTree as ET
     import zipfile
     import zipfile
 
 
+    import defusedxml.ElementTree as ET
+
     # Validate printer
     # Validate printer
     result = await db.execute(select(Printer).where(Printer.id == printer_id))
     result = await db.execute(select(Printer).where(Printer.id == printer_id))
     printer = result.scalar_one_or_none()
     printer = result.scalar_one_or_none()

+ 1 - 1
backend/app/services/archive.py

@@ -6,8 +6,8 @@ import shutil
 import zipfile
 import zipfile
 from datetime import datetime
 from datetime import datetime
 from pathlib import Path
 from pathlib import Path
-from xml.etree import ElementTree as ET
 
 
+from defusedxml import ElementTree as ET
 from sqlalchemy import and_, or_, select
 from sqlalchemy import and_, or_, select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.ext.asyncio import AsyncSession
 
 

+ 1 - 1
backend/app/services/print_scheduler.py

@@ -3,11 +3,11 @@
 import asyncio
 import asyncio
 import json
 import json
 import logging
 import logging
-import xml.etree.ElementTree as ET
 import zipfile
 import zipfile
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
 from pathlib import Path
 from pathlib import Path
 
 
+import defusedxml.ElementTree as ET
 from sqlalchemy import func, select
 from sqlalchemy import func, select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.ext.asyncio import AsyncSession
 
 

+ 1 - 0
requirements.txt

@@ -20,6 +20,7 @@ pyftpdlib>=2.0.0
 cryptography>=41.0.0
 cryptography>=41.0.0
 
 
 # 3MF Processing (standard zipfile is sufficient for Bambu 3MF files)
 # 3MF Processing (standard zipfile is sufficient for Bambu 3MF files)
+defusedxml>=0.7.0  # Safe XML parsing (prevents XXE attacks)
 
 
 # Excel Export
 # Excel Export
 openpyxl>=3.1.0
 openpyxl>=3.1.0