CVE-2021-43818 (Python, published 2021-12-13T18:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 7.1)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
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.
Repository: https://github.com/lxml/lxml
Files changed:
src/lxml/html/clean.py
src/lxml/html/tests/test_clean.py
Commit message: Cleaner: Remove SVG image data URLs since they can embed script content.

Reported as GHSL-2021-1038


Vulnerable code:
_is_image_dataurl = re.compile(
    r'^data:image/.+;base64', re.I).search
    r'(?:javascript|jscript|livescript|vbscript|data|about|mocha):',
    re.I).search
    if _is_image_dataurl(s):
        return None
    return _is_possibly_malicious_scheme(s)
# FIXME: should data: be blocked?

Fixed code:
_find_image_dataurls = re.compile(
    r'^data:image/(.+);base64,', re.I).findall
    r'(javascript|jscript|livescript|vbscript|data|about|mocha):',
    re.I).findall
# SVG images can contain script content
_is_unsafe_image_type = re.compile(r"(xml|svg)", re.I).findall

    is_image_url = False
    for image_type in _find_image_dataurls(s):
        is_image_url = True
        if _is_unsafe_image_type(image_type):
            return True
    if is_image_url:
        return False
    return bool(_is_possibly_malicious_scheme(s))
import base64
import gzip
    def test_svg_data_links(self):
        # Remove SVG images with potentially insecure content.
        svg = b'<svg onload="alert(123)" />'
        svgz = gzip.compress(svg)
        svg_b64 = base64.b64encode(svg).decode('ASCII')
        svgz_b64 = base64.b64encode(svgz).decode('ASCII')
        urls = [
            "data:image/svg+xml;base64," + svg_b64,
            "data:image/svg+xml-compressed;base64," + svgz_b64,
        ]
        for url in urls:
            html = '<img src="%s">' % url
            s = lxml.html.fragment_fromstring(html)

            cleaned = lxml.html.tostring(clean_html(s))
            self.assertEqual(
                b'<img src="">',
                cleaned,
                "%s  ->  %s" % (url, cleaned))

    def test_image_data_links(self):
        data = b'123'
        data_b64 = base64.b64encode(data).decode('ASCII')
        urls = [
            "data:image/jpeg;base64," + data_b64,
            "data:image/apng;base64," + data_b64,
            "data:image/png;base64," + data_b64,
            "data:image/gif;base64," + data_b64,
            "data:image/webp;base64," + data_b64,
            "data:image/bmp;base64," + data_b64,
            "data:image/tiff;base64," + data_b64,
            "data:image/x-icon;base64," + data_b64,
        ]
        for url in urls:
            html = '<img src="%s">' % url
            s = lxml.html.fragment_fromstring(html)

            cleaned = lxml.html.tostring(clean_html(s))
            self.assertEqual(
                html.encode("UTF-8"),
                cleaned,
                "%s  ->  %s" % (url, cleaned))