Commit d41f5d75 authored by Alexander Schrijver's avatar Alexander Schrijver
Browse files

Make defusedxml an optional dependency. If it's installed on the system, it

used used. Otherwise either lxml or Elementree is used.
parent bf55af4b7475
......@@ -13,6 +13,12 @@ the Office Open XML format.
All kudos to the PHPExcel team as openpyxl was initially based on PHPExcel.
Security
--------
By default openpyxl does not guard against quadratic blowup or billion laughs
xml attacks. To guard against these attacks install defusedxml.
Mailing List
------------
......
......@@ -2,7 +2,7 @@
from openpyxl.compat.numbers import NUMPY, PANDAS
from openpyxl.xml import LXML
from openpyxl.xml import DEFUSEDXML, LXML
from openpyxl.workbook import Workbook
from openpyxl.reader.excel import load_workbook
import openpyxl._constants as constants
......
......@@ -33,6 +33,10 @@ def pytest_runtest_setup(item):
pytest.skip("PIL is installed")
elif item.get_marker("not_py33"):
pytest.skip("Ordering is not a given in Python 3")
elif item.get_marker("defusedxml_required"):
from openpyxl import DEFUSEDXML
if not DEFUSEDXML:
pytest.skip("defusedxml is required to guard against these vulnerabilities")
elif item.get_marker("lxml_required"):
from openpyxl import LXML
if not LXML:
......
from __future__ import absolute_import
import pytest
from defusedxml.common import DefusedXmlException
from openpyxl.reader.excel import load_workbook
from .helper import modify_zip_file
@pytest.mark.defusedxml_required
@pytest.mark.parametrize("file, old, new", (
(
'xl/sharedStrings.xml',
......@@ -20,6 +20,7 @@ from .helper import modify_zip_file
),
))
def test_quadratic_blowup(datadir, file, old, new):
from defusedxml.common import DefusedXmlException
datadir.join("genuine").chdir()
attack_entity = (
......@@ -41,6 +42,7 @@ def test_quadratic_blowup(datadir, file, old, new):
# print(list(wb['Sheet1 - Text'].iter_rows())[0][0].value)
@pytest.mark.defusedxml_required
@pytest.mark.parametrize("file, old, new", (
(
'xl/sharedStrings.xml',
......@@ -54,6 +56,7 @@ def test_quadratic_blowup(datadir, file, old, new):
),
))
def test_billion_laughs(datadir, file, old, new):
from defusedxml.common import DefusedXmlException
attack_entity = (
'<?xml version="1.0" encoding="UTF-8"?>'
'<!DOCTYPE xmlbomb ['
......@@ -80,6 +83,7 @@ def test_billion_laughs(datadir, file, old, new):
# print(list(wb['Sheet1 - Text'].iter_rows())[0][0].value)
@pytest.mark.defusedxml_required
@pytest.mark.parametrize("file, old, new", (
(
'xl/sharedStrings.xml',
......@@ -97,6 +101,7 @@ def test_xxe_external_file(datadir, file, old, new):
I never was able to trigger this attack. The version of libxml2/lxml
(libxml2 2.9.3) I am using is not vulnerable.
"""
from defusedxml.common import DefusedXmlException
attack_entity = (
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<!DOCTYPE foo [ '
......@@ -112,6 +117,7 @@ def test_xxe_external_file(datadir, file, old, new):
pytest.raises(DefusedXmlException, load_workbook, new_file)
@pytest.mark.defusedxml_required
@pytest.mark.parametrize("file", (
'xl/sharedStrings.xml',
'[Content_Types].xml',
......@@ -121,6 +127,7 @@ def test_xxe_remote(datadir, file):
I never triggered this attack. I don't this this was ever an existing
vulnerability.
"""
from defusedxml.common import DefusedXmlException
attack_entity = (
'<?xml version="1.0" encoding="UTF-8"?>'
'<!DOCTYPE test [ '
......
......@@ -25,3 +25,19 @@ def lxml_env_set():
LXML = lxml_available() and lxml_env_set()
def defusedxml_available():
try:
import defusedxml # noqa
except ImportError:
return False
else:
return True
def defusedxml_env_set():
return os.environ.get("OPENPYXL_DEFUSEDXML", "True") == "True"
DEFUSEDXML = defusedxml_available() and defusedxml_env_set()
......@@ -11,7 +11,7 @@ from functools import partial
# compatibility
# package imports
from openpyxl import LXML
from openpyxl import DEFUSEDXML, LXML
if LXML is True:
from lxml.etree import (
......@@ -23,17 +23,24 @@ if LXML is True:
xmlfile,
XMLParser,
)
from defusedxml.common import DefusedXmlException
from defusedxml.cElementTree import iterparse
from defusedxml.lxml import fromstring as _fromstring, tostring
from lxml.etree import XMLSyntaxError
def fromstring(*args, **kwargs):
try:
return _fromstring(*args, **kwargs)
except XMLSyntaxError as e:
raise DefusedXmlException(str(e))
if DEFUSEDXML is True:
from defusedxml.common import DefusedXmlException
from defusedxml.cElementTree import iterparse
from defusedxml.lxml import fromstring as _fromstring, tostring
def fromstring(*args, **kwargs):
try:
return _fromstring(*args, **kwargs)
except XMLSyntaxError as e:
raise DefusedXmlException(str(e))
else:
from lxml.etree import fromstring, tostring
from xml.etree.cElementTree import iterparse
# do not resolve entities
safe_parser = XMLParser(resolve_entities=False)
fromstring = partial(fromstring, parser=safe_parser)
else:
try:
......@@ -44,11 +51,18 @@ else:
QName,
register_namespace
)
from defusedxml.cElementTree import (
fromstring,
tostring,
iterparse,
)
if DEFUSEDXML is True:
from defusedxml.cElementTree import (
fromstring,
tostring,
iterparse,
)
else:
from xml.etree.cElementTree import (
fromstring,
tostring,
iterparse
)
except ImportError:
from xml.etree.ElementTree import (
ElementTree,
......@@ -57,11 +71,18 @@ else:
QName,
register_namespace
)
from defusedxml.ElementTree import (
fromstring,
tostring,
iterparse,
)
if DEFUSEDXML is True:
from defusedxml.ElementTree import (
fromstring,
tostring,
iterparse,
)
else:
from xml.etree.ElementTree import (
fromstring,
tostring,
iterparse,
)
from et_xmlfile import xmlfile
......
import pytest
from io import BytesIO
from defusedxml.common import DefusedXmlException
from ..functions import fromstring, iterparse
......@@ -51,14 +50,18 @@ vulnerable_xml_strings = (
)
@pytest.mark.defusedxml_required
@pytest.mark.parametrize("xml_input", vulnerable_xml_strings)
def test_fromstring(xml_input):
from defusedxml.common import DefusedXmlException
with pytest.raises(DefusedXmlException):
fromstring(xml_input)
@pytest.mark.defusedxml_required
@pytest.mark.parametrize("xml_input", vulnerable_xml_strings)
def test_iterparse(xml_input):
from defusedxml.common import DefusedXmlException
with pytest.raises(DefusedXmlException):
f = BytesIO(xml_input)
list(iterparse(f))
......@@ -8,6 +8,7 @@ markers =
pil_not_installed: Run test only if PIL is not installed
not_py33: Do not run test on Python 3.
lxml_required: lxml required to run test
defusedxml_required: lxml required to run test
lxml_buffering: lxml >= 3.4.0 required
no_lxml: do not use lxml
pandas_required: Pandas required for the test
......
......@@ -59,7 +59,7 @@ setup(
license=__license__,
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
install_requires=[
'jdcal', 'et_xmlfile', 'defusedxml'
'jdcal', 'et_xmlfile',
],
project_urls={
'Documentation': 'https://openpyxl.readthedocs.io/en/stable/',
......
......@@ -79,6 +79,20 @@ setenv =
commands =
py.test {posargs}
[testenv:defusedlxml]
deps =
{[testenv]deps}
defusedxml
[testenv:defusednolxml]
deps =
{[testenv]deps}
defusedxml
setenv =
OPENPYXL_LXML = False
[testenv:keep_vba]
setenv =
OPENPYXL_KEEP_VBA = True
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment