Compare commits

..

2 commits

Author SHA1 Message Date
Charalampos Stratakis
420dc55c3e Fix CVE-2021-28957
missing input sanitization for formaction HTML5 attributes may lead to XSS

Resolves: rhbz#1941535
2021-05-21 14:31:06 +02:00
Mikolaj Izdebski
ae540ddc4b Fix mXSS vulnerability due to the use of improper parser
Resolves: CVE-2020-27783
2020-12-18 16:09:47 +01:00
13 changed files with 1974 additions and 1782 deletions

View file

@ -1 +0,0 @@
1

19
.gitignore vendored
View file

@ -54,22 +54,3 @@ lxml-2.2.7.tar.gz.asc
/lxml-4.4.0.tgz
/lxml-4.4.1.tgz
/lxml-4.5.1.tgz
/lxml-4.6.2.tar.gz
/lxml-4.6.3.tar.gz
/lxml-4.7.1.tar.gz
/lxml-4.9.1.tar.gz
/lxml-4.9.2.tar.gz
/lxml-4.9.2-no-isoschematron.tar.gz
/lxml-4.9.2-no-isoschematron-rng.tar.gz
/lxml-4.9.3-no-isoschematron-rng.tar.gz
/lxml-4.9.4-no-isoschematron-rng.tar.gz
/lxml-5.1.0-no-isoschematron-rng.tar.gz
/lxml-5.2.0-no-isoschematron-rng.tar.gz
/lxml-5.2.1-no-isoschematron-rng.tar.gz
/lxml-5.3.0-no-isoschematron-rng.tar.gz
/lxml-5.3.1-no-isoschematron-rng.tar.gz
/lxml-5.3.2-no-isoschematron-rng.tar.gz
/lxml-5.4.0-no-isoschematron-rng.tar.gz
/lxml-6.0.0-no-isoschematron-rng.tar.gz
/lxml-6.0.1-no-isoschematron-rng.tar.gz
/lxml-6.0.2-no-isoschematron-rng.tar.gz

View file

@ -0,0 +1,280 @@
From b90f1ec15cfd26684ef54eb19b536b010d6a740b Mon Sep 17 00:00:00 2001
From: Stefan Behnel <stefan_ml@behnel.de>
Date: Sat, 23 May 2020 09:34:22 +0200
Subject: [PATCH 1/2] Avoid globally overriding the libxml2 external entity
resolver and instead set it for each parser run. This improves the
interoperability with other users of libxml2 in the system, such as
libxmlsec.
---
CHANGES.txt | 11 +++++++++++
src/lxml/dtd.pxi | 6 ++++++
src/lxml/parser.pxi | 42 ++++++++++++++++++++++++++++++-----------
src/lxml/relaxng.pxi | 2 ++
src/lxml/schematron.pxi | 4 ++++
src/lxml/xinclude.pxi | 2 ++
src/lxml/xmlschema.pxi | 2 ++
src/lxml/xslt.pxi | 4 ++++
8 files changed, 62 insertions(+), 11 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 30e80599..07afb641 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -2,6 +2,17 @@
lxml changelog
==============
+4.5.2 (2020-0?-??)
+==================
+
+Bugs fixed
+----------
+
+* LP#1880251: Instead of globally overwriting the document loader in libxml2, lxml now
+ sets it per parser run, which improves the interoperability with other users of libxml2
+ such as libxmlsec.
+
+
4.5.1 (2020-05-19)
==================
diff --git a/src/lxml/dtd.pxi b/src/lxml/dtd.pxi
index 59529654..5dcb80c4 100644
--- a/src/lxml/dtd.pxi
+++ b/src/lxml/dtd.pxi
@@ -277,14 +277,20 @@ cdef class DTD(_Validator):
if _isString(file):
file = _encodeFilename(file)
with self._error_log:
+ orig_loader = _register_document_loader()
self._c_dtd = xmlparser.xmlParseDTD(NULL, _xcstr(file))
+ _reset_document_loader(orig_loader)
elif hasattr(file, 'read'):
+ orig_loader = _register_document_loader()
self._c_dtd = _parseDtdFromFilelike(file)
+ _reset_document_loader(orig_loader)
else:
raise DTDParseError, u"file must be a filename or file-like object"
elif external_id is not None:
with self._error_log:
+ orig_loader = _register_document_loader()
self._c_dtd = xmlparser.xmlParseDTD(<const_xmlChar*>external_id, NULL)
+ _reset_document_loader(orig_loader)
else:
raise DTDParseError, u"either filename or external ID required"
diff --git a/src/lxml/parser.pxi b/src/lxml/parser.pxi
index 22620373..3ed223bd 100644
--- a/src/lxml/parser.pxi
+++ b/src/lxml/parser.pxi
@@ -502,7 +502,15 @@ cdef xmlparser.xmlParserInput* _local_resolver(const_char* c_url, const_char* c_
cdef xmlparser.xmlExternalEntityLoader __DEFAULT_ENTITY_LOADER
__DEFAULT_ENTITY_LOADER = xmlparser.xmlGetExternalEntityLoader()
-xmlparser.xmlSetExternalEntityLoader(<xmlparser.xmlExternalEntityLoader>_local_resolver)
+
+cdef xmlparser.xmlExternalEntityLoader _register_document_loader() nogil:
+ cdef xmlparser.xmlExternalEntityLoader old = xmlparser.xmlGetExternalEntityLoader()
+ xmlparser.xmlSetExternalEntityLoader(<xmlparser.xmlExternalEntityLoader>_local_resolver)
+ return old
+
+cdef void _reset_document_loader(xmlparser.xmlExternalEntityLoader old) nogil:
+ xmlparser.xmlSetExternalEntityLoader(old)
+
############################################################
## Parsers
@@ -514,6 +522,7 @@ cdef class _ParserContext(_ResolverContext):
cdef _ErrorLog _error_log
cdef _ParserSchemaValidationContext _validator
cdef xmlparser.xmlParserCtxt* _c_ctxt
+ cdef xmlparser.xmlExternalEntityLoader _orig_loader
cdef python.PyThread_type_lock _lock
cdef _Document _doc
cdef bint _collect_ids
@@ -561,7 +570,7 @@ cdef class _ParserContext(_ResolverContext):
else:
xmlparser.xmlClearParserCtxt(self._c_ctxt)
- cdef int prepare(self) except -1:
+ cdef int prepare(self, bint set_document_loader=True) except -1:
cdef int result
if config.ENABLE_THREADING and self._lock is not NULL:
with nogil:
@@ -572,19 +581,24 @@ cdef class _ParserContext(_ResolverContext):
self._error_log.clear()
self._doc = None
self._c_ctxt.sax.serror = _receiveParserError
+ self._orig_loader = _register_document_loader() if set_document_loader else NULL
if self._validator is not None:
self._validator.connect(self._c_ctxt, self._error_log)
return 0
cdef int cleanup(self) except -1:
- if self._validator is not None:
- self._validator.disconnect()
- self._resetParserContext()
- self.clear()
- self._doc = None
- self._c_ctxt.sax.serror = NULL
- if config.ENABLE_THREADING and self._lock is not NULL:
- python.PyThread_release_lock(self._lock)
+ if self._orig_loader is not NULL:
+ _reset_document_loader(self._orig_loader)
+ try:
+ if self._validator is not None:
+ self._validator.disconnect()
+ self._resetParserContext()
+ self.clear()
+ self._doc = None
+ self._c_ctxt.sax.serror = NULL
+ finally:
+ if config.ENABLE_THREADING and self._lock is not NULL:
+ python.PyThread_release_lock(self._lock)
return 0
cdef object _handleParseResult(self, _BaseParser parser,
@@ -1286,7 +1300,7 @@ cdef class _FeedParser(_BaseParser):
pctxt = context._c_ctxt
error = 0
if not self._feed_parser_running:
- context.prepare()
+ context.prepare(set_document_loader=False)
self._feed_parser_running = 1
c_filename = (_cstr(self._filename)
if self._filename is not None else NULL)
@@ -1296,6 +1310,7 @@ cdef class _FeedParser(_BaseParser):
# however if we give it all we got, we'll have nothing for
# *mlParseChunk() and things go wrong.
buffer_len = 4 if py_buffer_len > 4 else <int>py_buffer_len
+ orig_loader = _register_document_loader()
if self._for_html:
error = _htmlCtxtResetPush(
pctxt, c_data, buffer_len, c_filename, c_encoding,
@@ -1304,6 +1319,7 @@ cdef class _FeedParser(_BaseParser):
xmlparser.xmlCtxtUseOptions(pctxt, self._parse_options)
error = xmlparser.xmlCtxtResetPush(
pctxt, c_data, buffer_len, c_filename, c_encoding)
+ _reset_document_loader(orig_loader)
py_buffer_len -= buffer_len
c_data += buffer_len
if error:
@@ -1321,7 +1337,9 @@ cdef class _FeedParser(_BaseParser):
buffer_len = <int>py_buffer_len
if self._for_html:
c_node = pctxt.node # last node where the parser stopped
+ orig_loader = _register_document_loader()
error = htmlparser.htmlParseChunk(pctxt, c_data, buffer_len, 0)
+ _reset_document_loader(orig_loader)
# and now for the fun part: move node names to the dict
if pctxt.myDoc:
fixup_error = _fixHtmlDictSubtreeNames(
@@ -1331,7 +1349,9 @@ cdef class _FeedParser(_BaseParser):
pctxt.myDoc.dict = pctxt.dict
xmlparser.xmlDictReference(pctxt.dict)
else:
+ orig_loader = _register_document_loader()
error = xmlparser.xmlParseChunk(pctxt, c_data, buffer_len, 0)
+ _reset_document_loader(orig_loader)
py_buffer_len -= buffer_len
c_data += buffer_len
diff --git a/src/lxml/relaxng.pxi b/src/lxml/relaxng.pxi
index d161ce46..6a82a295 100644
--- a/src/lxml/relaxng.pxi
+++ b/src/lxml/relaxng.pxi
@@ -64,7 +64,9 @@ cdef class RelaxNG(_Validator):
doc = None
filename = _encodeFilename(file)
with self._error_log:
+ orig_loader = _register_document_loader()
parser_ctxt = relaxng.xmlRelaxNGNewParserCtxt(_cstr(filename))
+ _reset_document_loader(orig_loader)
elif (_getFilenameForFile(file) or '')[-4:].lower() == '.rnc':
_require_rnc2rng()
rng_data_utf8 = _utf8(_rnc2rng.dumps(_rnc2rng.load(file)))
diff --git a/src/lxml/schematron.pxi b/src/lxml/schematron.pxi
index af4ba7f0..dfd2cc05 100644
--- a/src/lxml/schematron.pxi
+++ b/src/lxml/schematron.pxi
@@ -95,7 +95,9 @@ cdef class Schematron(_Validator):
filename = file
filename = _encodeFilename(filename)
with self._error_log:
+ orig_loader = _register_document_loader()
parser_ctxt = schematron.xmlSchematronNewParserCtxt(_cstr(filename))
+ _reset_document_loader(orig_loader)
else:
raise SchematronParseError, u"No tree or file given"
@@ -107,7 +109,9 @@ cdef class Schematron(_Validator):
try:
with self._error_log:
+ orig_loader = _register_document_loader()
self._c_schema = schematron.xmlSchematronParse(parser_ctxt)
+ _reset_document_loader(orig_loader)
finally:
schematron.xmlSchematronFreeParserCtxt(parser_ctxt)
diff --git a/src/lxml/xinclude.pxi b/src/lxml/xinclude.pxi
index f73afee6..6bac8292 100644
--- a/src/lxml/xinclude.pxi
+++ b/src/lxml/xinclude.pxi
@@ -49,11 +49,13 @@ cdef class XInclude:
if tree.LIBXML_VERSION < 20704 or not c_context:
__GLOBAL_PARSER_CONTEXT.pushImpliedContext(context)
with nogil:
+ orig_loader = _register_document_loader()
if c_context:
result = xinclude.xmlXIncludeProcessTreeFlagsData(
node._c_node, parse_options, c_context)
else:
result = xinclude.xmlXIncludeProcessTree(node._c_node)
+ _reset_document_loader(orig_loader)
if tree.LIBXML_VERSION < 20704 or not c_context:
__GLOBAL_PARSER_CONTEXT.popImpliedContext()
self._error_log.disconnect()
diff --git a/src/lxml/xmlschema.pxi b/src/lxml/xmlschema.pxi
index cc2c1928..ab26d935 100644
--- a/src/lxml/xmlschema.pxi
+++ b/src/lxml/xmlschema.pxi
@@ -77,7 +77,9 @@ cdef class XMLSchema(_Validator):
# resolve requests to the document's parser
__GLOBAL_PARSER_CONTEXT.pushImpliedContextFromParser(self._doc._parser)
with nogil:
+ orig_loader = _register_document_loader()
self._c_schema = xmlschema.xmlSchemaParse(parser_ctxt)
+ _reset_document_loader(orig_loader)
if self._doc is not None:
__GLOBAL_PARSER_CONTEXT.popImpliedContext()
xmlschema.xmlSchemaFreeParserCtxt(parser_ctxt)
diff --git a/src/lxml/xslt.pxi b/src/lxml/xslt.pxi
index e7b49600..d483cfa3 100644
--- a/src/lxml/xslt.pxi
+++ b/src/lxml/xslt.pxi
@@ -397,7 +397,9 @@ cdef class XSLT:
c_doc._private = <python.PyObject*>self._xslt_resolver_context
with self._error_log:
+ orig_loader = _register_document_loader()
c_style = xslt.xsltParseStylesheetDoc(c_doc)
+ _reset_document_loader(orig_loader)
if c_style is NULL or c_style.errors:
tree.xmlFreeDoc(c_doc)
@@ -633,8 +635,10 @@ cdef class XSLT:
if self._access_control is not None:
self._access_control._register_in_context(transform_ctxt)
with self._error_log, nogil:
+ orig_loader = _register_document_loader()
c_result = xslt.xsltApplyStylesheetUser(
self._c_style, c_input_doc, params, NULL, NULL, transform_ctxt)
+ _reset_document_loader(orig_loader)
return c_result
--
2.26.2

View file

@ -0,0 +1,75 @@
From 06fbba68024e0287b8e8f6da85ce840ffbfec3bc Mon Sep 17 00:00:00 2001
From: Mikolaj Izdebski <mizdebsk@redhat.com>
Date: Fri, 18 Dec 2020 16:08:43 +0100
Subject: [PATCH 2/2] Fix CVE-2020-27783: mXSS due to the use of improper
parser
Backported from upstream commits 89e7aad6e7ff9ecd88678ff25f885988b184b26e
and a105ab8dc262ec6735977c25c13f0bdfcdec72a7
---
src/lxml/html/clean.py | 25 +++++++++++++++++--------
1 file changed, 17 insertions(+), 8 deletions(-)
diff --git a/src/lxml/html/clean.py b/src/lxml/html/clean.py
index da1f8706..c4fbfaa3 100644
--- a/src/lxml/html/clean.py
+++ b/src/lxml/html/clean.py
@@ -61,12 +61,15 @@ __all__ = ['clean_html', 'clean', 'Cleaner', 'autolink', 'autolink_html',
# This is an IE-specific construct you can have in a stylesheet to
# run some Javascript:
-_css_javascript_re = re.compile(
- r'expression\s*\(.*?\)', re.S|re.I)
+_replace_css_javascript = re.compile(
+ r'expression\s*\(.*?\)', re.S|re.I).sub
# Do I have to worry about @\nimport?
-_css_import_re = re.compile(
- r'@\s*import', re.I)
+_replace_css_import = re.compile(
+ r'@\s*import', re.I).sub
+
+_looks_like_tag_content = re.compile(
+ r'</?[a-zA-Z]+|\son[a-zA-Z]+\s*=', re.ASCII).search
# All kinds of schemes besides just javascript: that can cause
# execution:
@@ -292,8 +295,8 @@ class Cleaner(object):
if not self.inline_style:
for el in _find_styled_elements(doc):
old = el.get('style')
- new = _css_javascript_re.sub('', old)
- new = _css_import_re.sub('', new)
+ new = _replace_css_javascript('', old)
+ new = _replace_css_import('', new)
if self._has_sneaky_javascript(new):
# Something tricky is going on...
del el.attrib['style']
@@ -305,9 +308,9 @@ class Cleaner(object):
el.drop_tree()
continue
old = el.text or ''
- new = _css_javascript_re.sub('', old)
+ new = _replace_css_javascript('', old)
# The imported CSS can do anything; we just can't allow:
- new = _css_import_re.sub('', old)
+ new = _replace_css_import('', new)
if self._has_sneaky_javascript(new):
# Something tricky is going on...
el.text = '/* deleted */'
@@ -522,6 +525,12 @@ class Cleaner(object):
return True
if 'expression(' in style:
return True
+ if '</noscript' in style:
+ # e.g. '<noscript><style><a title="</noscript><img src=x onerror=alert(1)>">'
+ return True
+ if _looks_like_tag_content(style):
+ # e.g. '<math><style><img src=x onerror=alert(1)></style></math>'
+ return True
return False
def clean_html(self, html):
--
2.26.2

View file

@ -0,0 +1,39 @@
diff --git a/src/lxml/html/defs.py b/src/lxml/html/defs.py
index b21a113..e40c808 100644
--- a/src/lxml/html/defs.py
+++ b/src/lxml/html/defs.py
@@ -21,6 +21,8 @@ link_attrs = frozenset([
'usemap',
# Not standard:
'dynsrc', 'lowsrc',
+ # HTML5 formaction
+ 'formaction'
])
# Not in the HTML 4 spec:
diff --git a/src/lxml/html/tests/test_clean.py b/src/lxml/html/tests/test_clean.py
index a193d99..87b7220 100644
--- a/src/lxml/html/tests/test_clean.py
+++ b/src/lxml/html/tests/test_clean.py
@@ -68,6 +68,21 @@ class CleanerTest(unittest.TestCase):
s = lxml.html.fromstring('<invalid tag>child</another>')
self.assertEqual('child', clean_html(s).text_content())
+ def test_formaction_attribute_in_button_input(self):
+ # The formaction attribute overrides the form's action and should be
+ # treated as a malicious link attribute
+ html = ('<form id="test"><input type="submit" formaction="javascript:alert(1)"></form>'
+ '<button form="test" formaction="javascript:alert(1)">X</button>')
+ expected = ('<div><form id="test"><input type="submit" formaction=""></form>'
+ '<button form="test" formaction="">X</button></div>')
+ cleaner = Cleaner(
+ forms=False,
+ safe_attrs_only=False,
+ )
+ self.assertEqual(
+ expected,
+ cleaner.clean_html(html))
+
def test_suite():
suite = unittest.TestSuite()

1642
changelog

File diff suppressed because it is too large Load diff

1
ci.fmf
View file

@ -1 +0,0 @@
resultsdb-testcase: separate

View file

@ -1,8 +0,0 @@
--- !Policy
product_versions:
- fedora-*
decision_contexts:
- bodhi_update_push_testing
- bodhi_update_push_stable
rules:
- !PassingTestCaseRule {test_case_name: fedora-ci.koji-build./plans/smoke.functional}

View file

@ -1,28 +0,0 @@
#! /bin/bash -ex
# Download a release of lxml (if missing) and remove the isoschematron module from it
version=$1
if [ -z "${version}" ]; then
echo "Usage: $0 VERSION" >& 2
echo "" >& 2
echo "example: $0 4.9.2" >& 2
exit 1
fi
versionedname=lxml-${version}
orig_archive=${versionedname}.tar.gz
new_archive=${versionedname}-no-isoschematron-rng.tar.gz
if [ ! -e ${orig_archive} ]; then
wget -N https://files.pythonhosted.org/packages/source/l/lxml/${orig_archive}
fi
deleted_directory=lxml-${version}/src/lxml/isoschematron/resources/rng
# tar --delete does not operate on compressed archives, so do
# gz decompression explicitly
gzip --decompress ${orig_archive}
tar -v --delete -f ${orig_archive//.gz} ${deleted_directory}
gzip -cf ${orig_archive//.gz} > ${new_archive}

View file

@ -1,7 +0,0 @@
import lxml.etree as et
s = '<foo><bar baz="xyzzy">a<![CDATA[b]]>c</bar></foo>'
x = et.fromstring(s)
t = x.find('bar').text
print(t)
if t != 'abc':
raise Exception()

View file

@ -1,12 +0,0 @@
summary: Basic smoke test
discover:
how: shell
tests:
- name: /smoke/import-python-module
test: |
python3 -c 'import importlib as il; print(il.import_module("lxml"))'
- name: /smoke/etree-fromstring
test: |
python3 plans/etree-fromstring.py
execute:
how: tmt

File diff suppressed because it is too large Load diff

View file

@ -1 +1 @@
SHA512 (lxml-6.0.2-no-isoschematron-rng.tar.gz) = dc89f75c3a3c828a46bcb2eefbebe8f98ce8072b9fb66f8ef81edbf2babed74e4c54e26392603319d60ab3bfab6a4795eedc85b28efba815797d43d53aae4060
SHA512 (lxml-4.5.1.tgz) = 5332d2b691b3d0def42b907a013fa814054dcd150ab6d7adf34891b70667b3e907f80c956a58671d7a31c63caa6110b2e007d2ed82b5fc706e6ad7b428742883