From ad3f3b2d9ecb2a7918b31397f8cd6b74aa9a889d Mon Sep 17 00:00:00 2001 From: Safi Date: Tue, 26 May 2026 20:32:14 +0100 Subject: [PATCH] harden XML parsing against billion-laughs DoS in extract_csproj and extract_lpk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stdlib ET does not cap entity expansion — a crafted .csproj or .lpk with nested internal entities can exhaust memory. Pre-screen input bytes for --- graphify/extract.py | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index 5b2b2fc2..88372d17 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -7389,6 +7389,30 @@ def extract_delphi_form(path: Path) -> dict: return {"nodes": nodes, "edges": edges, "input_tokens": 0, "output_tokens": 0} +# Size cap for project XML files we parse with stdlib ElementTree. +# Real .csproj/.fsproj/.vbproj/.lpk files are well under 2 MiB; anything +# larger is either malformed or hostile. +_PROJECT_XML_MAX_BYTES = 2 * 1024 * 1024 + + +def _project_xml_is_safe(src: bytes) -> bool: + """Reject XML that declares DTDs or entities. + + Stdlib ``xml.etree.ElementTree`` does not cap entity expansion, so a + crafted project file could trigger a billion-laughs style DoS. External + entity resolution is already disabled by pyexpat defaults, but rejecting + `` dict: """Extract package metadata from Lazarus .lpk package files (XML format). @@ -7408,8 +7432,18 @@ def extract_lazarus_package(path: Path) -> dict: """ try: import xml.etree.ElementTree as ET - text = path.read_text(encoding="utf-8", errors="replace") - xml_root = ET.fromstring(text) + src = path.read_bytes() + except OSError as e: + return {"nodes": [], "edges": [], "error": str(e)} + + if len(src) > _PROJECT_XML_MAX_BYTES: + return {"nodes": [], "edges": [], "error": "package file too large"} + if not _project_xml_is_safe(src): + return {"nodes": [], "edges": [], + "error": "refusing XML with DOCTYPE/ENTITY declaration"} + + try: + xml_root = ET.fromstring(src) except Exception as e: return {"nodes": [], "edges": [], "error": str(e)} @@ -7798,8 +7832,11 @@ def extract_csproj(path: Path) -> dict: except OSError: return {"nodes": [], "edges": [], "error": f"cannot read {path}"} - if len(src) > 2_097_152: + if len(src) > _PROJECT_XML_MAX_BYTES: return {"nodes": [], "edges": [], "error": "project file too large"} + if not _project_xml_is_safe(src): + return {"nodes": [], "edges": [], + "error": "refusing XML with DOCTYPE/ENTITY declaration"} try: tree = ET.fromstring(src)