Skip to content

Commit 6f2c105

Browse files
authored
Use defusedxml to prevent external entity exploits (#94)
* Use defusedxml to prevent external entity exploits Fixes #93
1 parent 42e4b3a commit 6f2c105

File tree

8 files changed

+87
-16
lines changed

8 files changed

+87
-16
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ jobs:
4040
python-version: "3.10"
4141

4242
- name: Install requirements
43-
run: python -m pip install wheel
43+
run: python -m pip install wheel setuptools build
4444

4545
- name: Build a distribution
46-
run: python setup.py sdist bdist_wheel
46+
run: python -m build
4747

4848
- name: Publish package to TestPyPI
4949
uses: pypa/gh-action-pypi-publish@master

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ Changelog
22
---------
33

44
Unreleased
5+
- (SECURITY) Use [defusedxml](https://github.com/tiran/defusedxml) to prevent XML SAX vulnerabilities ([#93](https://github.com/stchris/untangle/issues/93))
56

67
1.2.0
78
- (SECURITY) Prevent XML SAX vulnerability: External Entities injection ([#60](https://github.com/stchris/untangle/issues/60))

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include README.md

poetry.lock

Lines changed: 55 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ version = "1.2.0"
44
description = "Converts XML to Python objects"
55
authors = ["Christian Stefanescu <[email protected]>"]
66
license = "MIT"
7+
readme = "README.md"
78

89
[tool.poetry.dependencies]
910
python = "^3.7"
11+
defusedxml = "^0.7.1"
1012

1113
[tool.poetry.dev-dependencies]
1214
pytest = "^7.1.2"
1315
flake8 = "^4.0.1"
1416
black = "^22.6.0"
17+
build = "^0.8.0"
18+
setuptools = "^62.6.0"
19+
wheel = "^0.37.1"
1520

1621
[build-system]
1722
requires = ["poetry-core>=1.0.0"]

setup.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,24 @@
33

44
import untangle
55

6-
from setuptools import setup
6+
from setuptools import setup, find_packages
77
from pathlib import Path
88

9+
long_description = (Path(__file__).parent / "README.md").read_text()
10+
911
setup(
1012
name="untangle",
13+
packages=find_packages(),
1114
version=untangle.__version__,
1215
description="Convert XML documents into Python objects",
1316
long_description_content_type="text/markdown",
14-
long_description=(Path(__file__).parent / "README.md").read_text(),
17+
long_description=long_description,
1518
author="Christian Stefanescu",
1619
author_email="[email protected]",
1720
url="http://github.com/stchris//untangle",
1821
py_modules=["untangle"],
22+
install_requires=["defusedxml"],
23+
include_package_data=True,
1924
license="MIT",
2025
classifiers=[
2126
"Development Status :: 5 - Production/Stable",

tests/test_untangle.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import untangle
66
import xml
77

8+
import defusedxml
9+
810

911
class FromStringTestCase(unittest.TestCase):
1012
"""Basic parsing tests with input as string"""
@@ -364,16 +366,16 @@ class ParserFeatureTestCase(unittest.TestCase):
364366
def test_valid_feature(self):
365367
# xml.sax.handler.feature_external_ges -> load external general (text)
366368
# entities, such as DTDs
367-
doc = untangle.parse(self.bad_dtd_xml, feature_external_ges=False)
368-
self.assertEqual(doc.foo["bar"], "baz")
369+
with self.assertRaises(defusedxml.common.ExternalReferenceForbidden):
370+
untangle.parse(self.bad_dtd_xml)
369371

370372
def test_invalid_feature(self):
371373
with self.assertRaises(AttributeError):
372374
untangle.parse(self.bad_dtd_xml, invalid_feature=True)
373375

374376
def test_invalid_external_dtd(self):
375-
with self.assertRaises(IOError):
376-
untangle.parse(self.bad_dtd_xml, feature_external_ges=True)
377+
with self.assertRaises(defusedxml.common.ExternalReferenceForbidden):
378+
untangle.parse(self.bad_dtd_xml)
377379

378380

379381
class TestEquals(unittest.TestCase):
@@ -393,11 +395,11 @@ def test_list_equals(self):
393395
class TestExternalEntityExpansion(unittest.TestCase):
394396
def test_xxe(self):
395397
# from https://pypi.org/project/defusedxml/#external-entity-expansion-remote
396-
o = untangle.parse("tests/res/xxe.xml")
397-
assert o.root.cdata == ""
398+
with self.assertRaises(defusedxml.common.EntitiesForbidden):
399+
untangle.parse("tests/res/xxe.xml")
398400

399401

400402
if __name__ == "__main__":
401403
unittest.main()
402404

403-
# vim: set expandtab ts=4 sw=4:
405+
# vim: set expandtab ts=4 sw=4

untangle.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
"""
1616
import os
1717
import keyword
18-
from xml.sax import make_parser, handler
19-
from xml.sax.handler import feature_external_ges
18+
from defusedxml.sax import make_parser
19+
from xml.sax import handler
2020

2121

2222
try:
@@ -188,12 +188,15 @@ def parse(filename, **parser_features):
188188
189189
Raises ``xml.sax.SAXParseException`` if something goes wrong
190190
during parsing.
191+
192+
Raises ``defusedxml.common.EntitiesForbidden``
193+
or ``defusedxml.common.ExternalReferenceForbidden``
194+
when a potentially malicious entity load is attempted. See also
195+
https://github.com/tiran/defusedxml#attack-vectors
191196
"""
192197
if filename is None or (is_string(filename) and filename.strip()) == "":
193198
raise ValueError("parse() takes a filename, URL or XML string")
194199
parser = make_parser()
195-
# See https://github.com/stchris/untangle/issues/60
196-
parser.setFeature(feature_external_ges, False)
197200
for feature, value in parser_features.items():
198201
parser.setFeature(getattr(handler, feature), value)
199202
sax_handler = Handler()

0 commit comments

Comments
 (0)