diff --git a/copyparty/dxml.py b/copyparty/dxml.py
index c44b3afc..b49f060b 100644
--- a/copyparty/dxml.py
+++ b/copyparty/dxml.py
@@ -1,3 +1,6 @@
+# coding: utf-8
+from __future__ import print_function, unicode_literals
+
import importlib
import sys
import xml.etree.ElementTree as ET
@@ -8,6 +11,10 @@ if True: # pylint: disable=using-constant-test
from typing import Any, Optional
+class BadXML(Exception):
+ pass
+
+
def get_ET() -> ET.XMLParser:
pn = "xml.etree.ElementTree"
cn = "_elementtree"
@@ -34,7 +41,7 @@ def get_ET() -> ET.XMLParser:
XMLParser: ET.XMLParser = get_ET()
-class DXMLParser(XMLParser): # type: ignore
+class _DXMLParser(XMLParser): # type: ignore
def __init__(self) -> None:
tb = ET.TreeBuilder()
super(DXMLParser, self).__init__(target=tb)
@@ -49,8 +56,12 @@ class DXMLParser(XMLParser): # type: ignore
raise BadXML("{}, {}".format(a, ka))
-class BadXML(Exception):
- pass
+class _NG(XMLParser): # type: ignore
+ def __int__(self) -> None:
+ raise BadXML("dxml selftest failed")
+
+
+DXMLParser = _DXMLParser
def parse_xml(txt: str) -> ET.Element:
@@ -59,6 +70,40 @@ def parse_xml(txt: str) -> ET.Element:
return parser.close() # type: ignore
+def selftest() -> bool:
+ qbe = r"""
+]>
+&a;&a;&a;"""
+
+ emb = r"""
+]>
+&a;"""
+
+ # future-proofing; there's never been any known vulns
+ # regarding DTDs and ET.XMLParser, but might as well
+ # block them since webdav-clients don't use them
+ dtd = r"""
+a"""
+
+ for txt in (qbe, emb, dtd):
+ try:
+ parse_xml(txt)
+ t = "WARNING: dxml selftest failed:\n%s\n"
+ print(t % (txt,), file=sys.stderr)
+ return False
+ except BadXML:
+ pass
+
+ return True
+
+
+DXML_OK = selftest()
+if not DXML_OK:
+ DXMLParser = _NG
+
+
def mktnod(name: str, text: str) -> ET.Element:
el = ET.Element(name)
el.text = text
diff --git a/copyparty/svchub.py b/copyparty/svchub.py
index ae89e7df..01b8b781 100644
--- a/copyparty/svchub.py
+++ b/copyparty/svchub.py
@@ -61,6 +61,7 @@ from .util import (
alltrace,
ansi_re,
build_netmap,
+ expat_ver,
load_ipu,
min_ex,
mp,
@@ -696,6 +697,15 @@ class SvcHub(object):
if self.args.bauth_last:
self.log("root", "WARNING: ignoring --bauth-last due to --no-bauth", 3)
+ if not self.args.no_dav:
+ from .dxml import DXML_OK
+
+ if not DXML_OK:
+ if not self.args.no_dav:
+ self.args.no_dav = True
+ t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
+ self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
+
def _process_config(self) -> bool:
al = self.args
diff --git a/copyparty/util.py b/copyparty/util.py
index 979b6a5f..aafe224b 100644
--- a/copyparty/util.py
+++ b/copyparty/util.py
@@ -495,6 +495,15 @@ def py_desc() -> str:
)
+def expat_ver() -> str:
+ try:
+ import pyexpat
+
+ return ".".join([str(x) for x in pyexpat.version_info])
+ except:
+ return "?"
+
+
def _sqlite_ver() -> str:
assert sqlite3 # type: ignore # !rm
try:
diff --git a/tests/test_dxml.py b/tests/test_dxml.py
index 2dc23f6e..309528a5 100644
--- a/tests/test_dxml.py
+++ b/tests/test_dxml.py
@@ -20,7 +20,8 @@ def _parse(txt):
class TestDXML(unittest.TestCase):
- def test1(self):
+ def test_qbe(self):
+ # allowed by default; verify that we stopped it
txt = r"""
]>
@@ -28,7 +29,8 @@ class TestDXML(unittest.TestCase):
_parse(txt)
ET.fromstring(txt)
- def test2(self):
+ def test_ent_file(self):
+ # NOT allowed by default; should still be blocked
txt = r"""
]>
@@ -40,6 +42,25 @@ class TestDXML(unittest.TestCase):
except ET.ParseError:
pass
+ def test_ent_ext(self):
+ # NOT allowed by default; should still be blocked
+ txt = r"""
+]>
+ⅇ"""
+ _parse(txt)
+
+ def test_dtd(self):
+ # allowed by default; verify that we stopped it
+ txt = r"""
+a"""
+ _parse(txt)
+ ET.fromstring(txt)
+
+ ##
+ ## end of negative/security tests; the rest is functional
+ ##
+
def test3(self):
txt = r"""