CVE-2021-43818: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: lxml is a library for processing XML and HTML in the Python language. Prior to version 4.6.5, the HTML Cleaner in lxml.html lets certain crafted script content pass through, as well as script content in SVG files embedded using data URIs. Users that employ the HTML cleaner in a security relevant context should upgrade to lxml 4.6.5 to receive a patch. There are no known workarounds available.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 7.1
Published: 2021-12-13T18:15Z
Repository: https://github.com/lxml/lxml
Language: Python
Files changed: src/lxml/html/clean.py, src/lxml/html/tests/test_clean.py
Commit date: 2021-11-11 13:19:30 +0100
Commit message: Cleaner: Prevent "@import" from re-occurring in the CSS after replacements, e.g. "@@importimport".

Reported as GHSL-2021-1037

Diff: diff --git a/src/lxml/html/clean.py b/src/lxml/html/clean.py
index 0494357e5..25844e873 100644
--- a/src/lxml/html/clean.py
+++ b/src/lxml/html/clean.py
@@ -539,10 +539,12 @@ class Cleaner(object):
         style = style.lower()
         if 'javascript:' in style:
             return True
         if 'expression(' in style:
             return True
+        if '@import' 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>'
diff --git a/src/lxml/html/tests/test_clean.py b/src/lxml/html/tests/test_clean.py
index 45c2e83ab..d395d5141 100644
--- a/src/lxml/html/tests/test_clean.py
+++ b/src/lxml/html/tests/test_clean.py
@@ -121,10 +121,30 @@ class CleanerTest(unittest.TestCase):
 
         self.assertEqual(
             b'<math><style>/* deleted */</style></math>',
             lxml.html.tostring(clean_html(s)))
 
+    def test_sneaky_import_in_style(self):
+        # Prevent "@@importimport" -> "@import" replacement.
+        style_codes = [
+            "@@importimport(extstyle.css)",
+            "@ @  import import(extstyle.css)",
+            "@ @ importimport(extstyle.css)",
+            "@@  import import(extstyle.css)",
+            "@ @import import(extstyle.css)",
+            "@@importimport()",
+        ]
+        for style_code in style_codes:
+            html = '<style>%s</style>' % style_code
+            s = lxml.html.fragment_fromstring(html)
+
+            cleaned = lxml.html.tostring(clean_html(s))
+            self.assertEqual(
+                b'<style>/* deleted */</style>',
+                cleaned,
+                "%s  ->  %s" % (style_code, cleaned))
+
     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>')

Fixed code:         if '@import' in style:
            return True
    def test_sneaky_import_in_style(self):
        # Prevent "@@importimport" -> "@import" replacement.
        style_codes = [
            "@@importimport(extstyle.css)",
            "@ @  import import(extstyle.css)",
            "@ @ importimport(extstyle.css)",
            "@@  import import(extstyle.css)",
            "@ @import import(extstyle.css)",
            "@@importimport()",
        ]
        for style_code in style_codes:
            html = '<style>%s</style>' % style_code
            s = lxml.html.fragment_fromstring(html)

            cleaned = lxml.html.tostring(clean_html(s))
            self.assertEqual(
                b'<style>/* deleted */</style>',
                cleaned,
                "%s  ->  %s" % (style_code, cleaned))