Merge pull request #296 from aaronjmars/security/xxe-defusedxml

fix(security): route untrusted XML parsing through defusedxml
This commit is contained in:
Yuhao
2026-05-20 23:33:04 +08:00
committed by GitHub
17 changed files with 37 additions and 19 deletions
@@ -10,6 +10,7 @@ import time
from pathlib import Path
from typing import Optional
from xml.etree import ElementTree as ET
from defusedxml.ElementTree import fromstring as _defused_fromstring
from ..utils import drawio_xml
@@ -93,7 +94,7 @@ class Session:
return False
self._redo_stack.append(self._snapshot())
prev = self._undo_stack.pop()
self.root = ET.fromstring(prev)
self.root = _defused_fromstring(prev)
self._modified = bool(self._undo_stack)
return True
@@ -103,7 +104,7 @@ class Session:
return False
self._undo_stack.append(self._snapshot())
nxt = self._redo_stack.pop()
self.root = ET.fromstring(nxt)
self.root = _defused_fromstring(nxt)
self._modified = True
return True
@@ -26,6 +26,7 @@ Structure:
import os
import time
from xml.etree import ElementTree as ET
from defusedxml.ElementTree import parse as _defused_parse
from typing import Optional
@@ -37,7 +38,7 @@ def parse_drawio(path: str) -> ET.Element:
"""Parse a .drawio XML file. Returns the root <mxfile> element."""
if not os.path.exists(path):
raise FileNotFoundError(f"File not found: {path}")
tree = ET.parse(path)
tree = _defused_parse(path)
return tree.getroot()
+1
View File
@@ -36,6 +36,7 @@ setup(
install_requires=[
"click>=8.0.0",
"prompt-toolkit>=3.0.0",
"defusedxml>=0.7.1",
],
extras_require={
"dev": [
@@ -5,6 +5,7 @@ and style string helpers.
"""
import xml.etree.ElementTree as ET
from defusedxml.ElementTree import parse as _defused_parse, fromstring as _defused_fromstring
import re
from typing import Dict, Optional, Any
@@ -79,12 +80,12 @@ def create_svg_element(
def parse_svg(svg_string: str) -> ET.Element:
"""Parse an SVG string into an ElementTree Element."""
return ET.fromstring(svg_string)
return _defused_fromstring(svg_string)
def parse_svg_file(path: str) -> ET.Element:
"""Parse an SVG file into an ElementTree Element."""
tree = ET.parse(path)
tree = _defused_parse(path)
return tree.getroot()
+1
View File
@@ -36,6 +36,7 @@ setup(
install_requires=[
"click>=8.0.0",
"prompt-toolkit>=3.0.0",
"defusedxml>=0.7.1",
],
extras_require={
"dev": [
@@ -4,6 +4,7 @@ import os
import tempfile
import zipfile
import xml.etree.ElementTree as ET
from defusedxml.ElementTree import fromstring as _defused_fromstring
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
@@ -125,7 +126,7 @@ def import_odf(
return project
try:
root = ET.fromstring(content_xml)
root = _defused_fromstring(content_xml)
except ET.ParseError as e:
raise ValueError(f"Invalid ODF content.xml in: {path}") from e
if inferred_type == "writer":
@@ -157,7 +158,7 @@ def _apply_metadata(project: Dict[str, Any], meta_xml: str, source_path: str) ->
return
try:
root = ET.fromstring(meta_xml)
root = _defused_fromstring(meta_xml)
except ET.ParseError as e:
raise ValueError(f"Invalid ODF meta.xml in: {source_path}") from e
@@ -14,6 +14,7 @@ Key ODF structure:
import os
import zipfile
import xml.etree.ElementTree as ET
from defusedxml.ElementTree import fromstring as _defused_fromstring
from typing import Dict, Any, Optional
from datetime import datetime
@@ -660,21 +661,21 @@ def validate_odf(path: str) -> Dict[str, Any]:
# Validate XML in content.xml
if "content.xml" in names:
try:
ET.fromstring(zf.read("content.xml"))
_defused_fromstring(zf.read("content.xml"))
except ET.ParseError as e:
errors.append(f"Invalid XML in content.xml: {e}")
# Validate XML in styles.xml
if "styles.xml" in names:
try:
ET.fromstring(zf.read("styles.xml"))
_defused_fromstring(zf.read("styles.xml"))
except ET.ParseError as e:
errors.append(f"Invalid XML in styles.xml: {e}")
# Validate XML in meta.xml
if "meta.xml" in names:
try:
ET.fromstring(zf.read("meta.xml"))
_defused_fromstring(zf.read("meta.xml"))
except ET.ParseError as e:
errors.append(f"Invalid XML in meta.xml: {e}")
+1
View File
@@ -36,6 +36,7 @@ setup(
install_requires=[
"click>=8.0.0",
"prompt-toolkit>=3.0.0",
"defusedxml>=0.7.1",
],
extras_require={
"dev": [
@@ -124,7 +124,8 @@ class FileTransformBackend(Backend):
def _xml_set_attr(self, p: dict) -> dict:
"""Set an XML element attribute matched by XPath."""
from xml.etree import ElementTree as ET
tree = ET.parse(p["input_file"])
from defusedxml.ElementTree import parse as _defused_parse
tree = _defused_parse(p["input_file"])
root = tree.getroot()
elements = root.findall(p["xpath"])
if not elements:
@@ -137,7 +138,8 @@ class FileTransformBackend(Backend):
def _xml_get_attr(self, p: dict) -> dict:
"""Get an XML element attribute matched by XPath."""
from xml.etree import ElementTree as ET
tree = ET.parse(p["input_file"])
from defusedxml.ElementTree import parse as _defused_parse
tree = _defused_parse(p["input_file"])
root = tree.getroot()
elements = root.findall(p["xpath"])
values = [el.get(p["attr"]) for el in elements]
+1
View File
@@ -38,6 +38,7 @@ setup(
"click>=8.0.0",
"prompt-toolkit>=3.0.0",
"PyYAML>=6.0",
"defusedxml>=0.7.1",
],
extras_require={
"dev": [
@@ -6,6 +6,7 @@ Handles reading and writing .mscz (ZIP containing .mscx XML) and
import os
import xml.etree.ElementTree as ET
from defusedxml.ElementTree import parse as _defused_parse, fromstring as _defused_fromstring
import zipfile
from pathlib import Path
@@ -91,7 +92,7 @@ def read_mscz(path: str) -> dict:
if name.endswith(".mscx"):
result["mscx_filename"] = name
xml_bytes = zf.read(name)
result["mscx"] = ET.ElementTree(ET.fromstring(xml_bytes))
result["mscx"] = ET.ElementTree(_defused_fromstring(xml_bytes))
elif name == "score_style.mss" or name.endswith("/score_style.mss"):
result["style"] = zf.read(name).decode("utf-8")
elif name == "audiosettings.json" or name.endswith("/audiosettings.json"):
@@ -154,7 +155,7 @@ def read_mxl(path: str) -> ET.ElementTree:
for name in zf.namelist():
if name.endswith(".xml") and not name.startswith("META-INF"):
xml_bytes = zf.read(name)
return ET.ElementTree(ET.fromstring(xml_bytes))
return ET.ElementTree(_defused_fromstring(xml_bytes))
raise ValueError(f"No MusicXML file found inside {path}")
@@ -344,6 +345,6 @@ def read_score_tree(path: str) -> ET.ElementTree:
elif fmt == "mxl":
return read_mxl(path)
elif fmt == "musicxml":
return ET.parse(path)
return _defused_parse(path)
else:
raise ValueError(f"Cannot read XML tree from format: {fmt} ({path})")
+1
View File
@@ -33,6 +33,7 @@ setup(
install_requires=[
"click>=8.0.0",
"prompt-toolkit>=3.0.0",
"defusedxml>=0.7.1",
],
extras_require={
"dev": [
@@ -10,6 +10,7 @@ import time
from pathlib import Path
from typing import Optional
import xml.etree.ElementTree as ET
from defusedxml.ElementTree import fromstring as _defused_fromstring
from ..utils import mlt_xml
@@ -113,7 +114,7 @@ class Session:
self._redo_stack.append(self._snapshot())
prev = self._undo_stack.pop()
mlt_xml._clear_parent_map()
self.root = ET.fromstring(prev)
self.root = _defused_fromstring(prev)
mlt_xml._register_tree(self.root)
self._resolve_refs()
self._modified = bool(self._undo_stack)
@@ -126,7 +127,7 @@ class Session:
self._undo_stack.append(self._snapshot())
nxt = self._redo_stack.pop()
mlt_xml._clear_parent_map()
self.root = ET.fromstring(nxt)
self.root = _defused_fromstring(nxt)
mlt_xml._register_tree(self.root)
self._resolve_refs()
self._modified = True
@@ -7,6 +7,7 @@ xml.etree.ElementTree from the Python standard library.
import copy
import uuid
import xml.etree.ElementTree as ET
from defusedxml.ElementTree import parse as _defused_parse
from typing import Optional
# Global parent mapping because ET.Element has no getparent().
@@ -51,7 +52,7 @@ def new_id(prefix: str = "producer") -> str:
def parse_mlt(filepath: str) -> ET.Element:
"""Parse an MLT XML file and return the root element."""
_clear_parent_map()
tree = ET.parse(filepath)
tree = _defused_parse(filepath)
root = tree.getroot()
_register_tree(root)
return root
+1
View File
@@ -36,6 +36,7 @@ setup(
install_requires=[
"click>=8.0.0",
"prompt-toolkit>=3.0.0",
"defusedxml>=0.7.1",
],
extras_require={
"dev": [
@@ -1,6 +1,7 @@
from __future__ import annotations
import xml.etree.ElementTree as ET
from defusedxml.ElementTree import parse as _defused_parse
from pathlib import Path
from typing import Any
@@ -236,7 +237,7 @@ def list_styles(runtime: RuntimeContext) -> list[dict[str, Any]]:
styles: list[dict[str, Any]] = []
for path in sorted(styles_dir.glob("*.csl")):
try:
root = ET.parse(path).getroot()
root = _defused_parse(path).getroot()
except ET.ParseError:
styles.append({"path": str(path), "id": None, "title": path.stem, "valid": False})
continue
+1
View File
@@ -45,6 +45,7 @@ setup(
install_requires=[
"click>=8.0.0",
"prompt-toolkit>=3.0.0",
"defusedxml>=0.7.1",
],
extras_require={
"dev": [