CVE-2026-28348, CVE-2026-28350 (bsc#1259378, bsc#1259379) #2
110
CVE-2026-28348.patch
Normal file
110
CVE-2026-28348.patch
Normal file
@@ -0,0 +1,110 @@
|
||||
From 2ef732667ddbc74ea59847bcf24b75809aaeed3b Mon Sep 17 00:00:00 2001
|
||||
From: Lumir Balhar <lbalhar@redhat.com>
|
||||
Date: Wed, 25 Feb 2026 22:35:58 +0100
|
||||
Subject: [PATCH] Implement unicode escape decoding
|
||||
|
||||
Unicode escapes in CSS were not properly decoded before security
|
||||
checks. This prevents attackers from bypassing filters using
|
||||
escape sequences.
|
||||
---
|
||||
CHANGES.rst | 7 ++++++
|
||||
lxml_html_clean/clean.py | 22 +++++++++++++++++-
|
||||
tests/test_clean.py | 48 ++++++++++++++++++++++++++++++++++++++++
|
||||
3 files changed, 76 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/lxml_html_clean/clean.py b/lxml_html_clean/clean.py
|
||||
index 3eeda47..5424d9f 100644
|
||||
--- a/lxml_html_clean/clean.py
|
||||
+++ b/lxml_html_clean/clean.py
|
||||
@@ -578,6 +578,26 @@ def _remove_javascript_link(self, link):
|
||||
_comments_re = re.compile(r'/\*.*?\*/', re.S)
|
||||
_find_comments = _comments_re.finditer
|
||||
_substitute_comments = _comments_re.sub
|
||||
+ _css_unicode_escape_re = re.compile(r'\\([0-9a-fA-F]{1,6})\s?')
|
||||
+
|
||||
+ def _decode_css_unicode_escapes(self, style):
|
||||
+ """
|
||||
+ Decode CSS Unicode escape sequences like \\69 or \\000069 to their
|
||||
+ actual character values. This prevents bypassing security checks
|
||||
+ using CSS escape sequences.
|
||||
+
|
||||
+ CSS escape syntax: backslash followed by 1-6 hex digits,
|
||||
+ optionally followed by a whitespace character.
|
||||
+ """
|
||||
+ def replace_escape(match):
|
||||
+ hex_value = match.group(1)
|
||||
+ try:
|
||||
+ return chr(int(hex_value, 16))
|
||||
+ except (ValueError, OverflowError):
|
||||
+ # Invalid unicode codepoint, keep original
|
||||
+ return match.group(0)
|
||||
+
|
||||
+ return self._css_unicode_escape_re.sub(replace_escape, style)
|
||||
|
||||
def _has_sneaky_javascript(self, style):
|
||||
"""
|
||||
@@ -591,7 +611,7 @@ def _has_sneaky_javascript(self, style):
|
||||
more sneaky attempts.
|
||||
"""
|
||||
style = self._substitute_comments('', style)
|
||||
- style = style.replace('\\', '')
|
||||
+ style = self._decode_css_unicode_escapes(style)
|
||||
style = _substitute_whitespace('', style)
|
||||
style = style.lower()
|
||||
if _has_javascript_scheme(style):
|
||||
diff --git a/tests/test_clean.py b/tests/test_clean.py
|
||||
index 64ad52d..d1ebcb1 100644
|
||||
--- a/tests/test_clean.py
|
||||
+++ b/tests/test_clean.py
|
||||
@@ -393,3 +393,51 @@ def test_possibly_invalid_url_without_whitelist(self):
|
||||
self.assertEqual(len(w), 0)
|
||||
self.assertNotIn("google.com", result)
|
||||
self.assertNotIn("example.com", result)
|
||||
+
|
||||
+ def test_unicode_escape_in_style(self):
|
||||
+ # Test that CSS Unicode escapes are properly decoded before security checks
|
||||
+ # This prevents attackers from bypassing filters using escape sequences
|
||||
+ # CSS escape syntax: \HHHHHH where H is a hex digit (1-6 digits)
|
||||
+
|
||||
+ # Test inline style attributes (requires safe_attrs_only=False)
|
||||
+ cleaner = Cleaner(safe_attrs_only=False)
|
||||
+ inline_style_cases = [
|
||||
+ # \6a\61\76\61\73\63\72\69\70\74 = "javascript"
|
||||
+ ('<div style="background: url(\\6a\\61\\76\\61\\73\\63\\72\\69\\70\\74:alert(1))">test</div>', '<div>test</div>'),
|
||||
+ # \69 = 'i', so \69mport = "import"
|
||||
+ ('<div style="@\\69mport url(evil.css)">test</div>', '<div>test</div>'),
|
||||
+ # \69 with space after = 'i', space consumed as part of escape
|
||||
+ ('<div style="@\\69 mport url(evil.css)">test</div>', '<div>test</div>'),
|
||||
+ # \65\78\70\72\65\73\73\69\6f\6e = "expression"
|
||||
+ ('<div style="\\65\\78\\70\\72\\65\\73\\73\\69\\6f\\6e(alert(1))">test</div>', '<div>test</div>'),
|
||||
+ ]
|
||||
+
|
||||
+ for html, expected in inline_style_cases:
|
||||
+ with self.subTest(html=html):
|
||||
+ cleaned = cleaner.clean_html(html)
|
||||
+ self.assertEqual(expected, cleaned)
|
||||
+
|
||||
+ # Test <style> tag content (uses default clean_html)
|
||||
+ style_tag_cases = [
|
||||
+ # Unicode-escaped "javascript:" in url()
|
||||
+ '<style>url(\\6a\\61\\76\\61\\73\\63\\72\\69\\70\\74:alert(1))</style>',
|
||||
+ # Unicode-escaped "javascript:" without url()
|
||||
+ '<style>\\6a\\61\\76\\61\\73\\63\\72\\69\\70\\74:alert(1)</style>',
|
||||
+ # Unicode-escaped "expression"
|
||||
+ '<style>\\65\\78\\70\\72\\65\\73\\73\\69\\6f\\6e(alert(1))</style>',
|
||||
+ # Unicode-escaped @import with 'i'
|
||||
+ '<style>@\\69mport url(evil.css)</style>',
|
||||
+ # Unicode-escaped "data:" scheme
|
||||
+ '<style>url(\\64\\61\\74\\61:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9YWxlcnQoMSk+)</style>',
|
||||
+ # Space after escape is consumed: \69 mport = "import"
|
||||
+ '<style>@\\69 mport url(evil.css)</style>',
|
||||
+ # 6-digit escape: \000069 = 'i'
|
||||
+ '<style>@\\000069mport url(evil.css)</style>',
|
||||
+ # 6-digit escape with space
|
||||
+ '<style>@\\000069 mport url(evil.css)</style>',
|
||||
+ ]
|
||||
+
|
||||
+ for html in style_tag_cases:
|
||||
+ with self.subTest(html=html):
|
||||
+ cleaned = clean_html(html)
|
||||
+ self.assertEqual('<div><style>/* deleted */</style></div>', cleaned)
|
||||
91
CVE-2026-28350.patch
Normal file
91
CVE-2026-28350.patch
Normal file
@@ -0,0 +1,91 @@
|
||||
From 9c5612ca33b941eec4178abf8a5294b103403f34 Mon Sep 17 00:00:00 2001
|
||||
From: Lumir Balhar <lbalhar@redhat.com>
|
||||
Date: Wed, 25 Feb 2026 22:57:28 +0100
|
||||
Subject: [PATCH] Remove <base> tags to prevent URL hijacking attacks
|
||||
|
||||
<base> tags are now automatically removed whenever <head> is removed to
|
||||
prevent URL hijacking attacks. According to HTML spec, <base> must be in
|
||||
<head>, but browsers may interpret misplaced <base> tags, allowing
|
||||
attackers to redirect all relative URLs to malicious servers.
|
||||
---
|
||||
CHANGES.rst | 5 +++++
|
||||
lxml_html_clean/clean.py | 6 +++++
|
||||
tests/test_clean.py | 48 ++++++++++++++++++++++++++++++++++++++++
|
||||
3 files changed, 59 insertions(+)
|
||||
|
||||
diff --git a/lxml_html_clean/clean.py b/lxml_html_clean/clean.py
|
||||
index 5424d9f..6f95b26 100644
|
||||
--- a/lxml_html_clean/clean.py
|
||||
+++ b/lxml_html_clean/clean.py
|
||||
@@ -422,6 +422,12 @@ def __call__(self, doc):
|
||||
if self.annoying_tags:
|
||||
remove_tags.update(('blink', 'marquee'))
|
||||
|
||||
+ # Remove <base> tags whenever <head> is being removed.
|
||||
+ # According to HTML spec, <base> must be in <head>, but browsers
|
||||
+ # may interpret it even when misplaced, allowing URL hijacking attacks.
|
||||
+ if 'head' in kill_tags or 'head' in remove_tags:
|
||||
+ kill_tags.add('base')
|
||||
+
|
||||
_remove = deque()
|
||||
_kill = deque()
|
||||
for el in doc.iter():
|
||||
diff --git a/tests/test_clean.py b/tests/test_clean.py
|
||||
index d1ebcb1..93f6da1 100644
|
||||
--- a/tests/test_clean.py
|
||||
+++ b/tests/test_clean.py
|
||||
@@ -394,6 +394,54 @@ def test_possibly_invalid_url_without_whitelist(self):
|
||||
self.assertNotIn("google.com", result)
|
||||
self.assertNotIn("example.com", result)
|
||||
|
||||
+ def test_base_tag_removed_with_page_structure(self):
|
||||
+ # Test that <base> tags are removed when page_structure=True (default)
|
||||
+ # This prevents URL hijacking attacks where <base> redirects all relative URLs
|
||||
+
|
||||
+ test_cases = [
|
||||
+ # <base> in proper location (inside <head>)
|
||||
+ '<html><head><base href="http://evil.com/"></head><body><a href="page.html">link</a></body></html>',
|
||||
+ # <base> outside <head>
|
||||
+ '<div><base href="http://evil.com/"><a href="page.html">link</a></div>',
|
||||
+ # Multiple <base> tags
|
||||
+ '<base href="http://evil.com/"><div><base href="http://evil2.com/"></div>',
|
||||
+ # <base> with target attribute
|
||||
+ '<base target="_blank"><div>content</div>',
|
||||
+ # <base> at various positions
|
||||
+ '<html><base href="http://evil.com/"><body>test</body></html>',
|
||||
+ ]
|
||||
+
|
||||
+ for html in test_cases:
|
||||
+ with self.subTest(html=html):
|
||||
+ cleaned = clean_html(html)
|
||||
+ # Verify <base> tag is completely removed
|
||||
+ self.assertNotIn('base', cleaned.lower())
|
||||
+ self.assertNotIn('evil.com', cleaned)
|
||||
+ self.assertNotIn('evil2.com', cleaned)
|
||||
+
|
||||
+ def test_base_tag_kept_when_page_structure_false(self):
|
||||
+ # When page_structure=False and head is not removed, <base> should be kept
|
||||
+ cleaner = Cleaner(page_structure=False)
|
||||
+ html = '<html><head><base href="http://example.com/"></head><body>test</body></html>'
|
||||
+ cleaned = cleaner.clean_html(html)
|
||||
+ self.assertIn('<base href="http://example.com/">', cleaned)
|
||||
+
|
||||
+ def test_base_tag_removed_when_head_in_remove_tags(self):
|
||||
+ # Even with page_structure=False, <base> should be removed if head is manually removed
|
||||
+ cleaner = Cleaner(page_structure=False, remove_tags=['head'])
|
||||
+ html = '<html><head><base href="http://evil.com/"></head><body>test</body></html>'
|
||||
+ cleaned = cleaner.clean_html(html)
|
||||
+ self.assertNotIn('base', cleaned.lower())
|
||||
+ self.assertNotIn('evil.com', cleaned)
|
||||
+
|
||||
+ def test_base_tag_removed_when_head_in_kill_tags(self):
|
||||
+ # Even with page_structure=False, <base> should be removed if head is in kill_tags
|
||||
+ cleaner = Cleaner(page_structure=False, kill_tags=['head'])
|
||||
+ html = '<html><head><base href="http://evil.com/"></head><body>test</body></html>'
|
||||
+ cleaned = cleaner.clean_html(html)
|
||||
+ self.assertNotIn('base', cleaned.lower())
|
||||
+ self.assertNotIn('evil.com', cleaned)
|
||||
+
|
||||
def test_unicode_escape_in_style(self):
|
||||
# Test that CSS Unicode escapes are properly decoded before security checks
|
||||
# This prevents attackers from bypassing filters using escape sequences
|
||||
@@ -1,3 +1,13 @@
|
||||
-------------------------------------------------------------------
|
||||
Tue Mar 10 12:59:11 UTC 2026 - Nico Krapp <nico.krapp@suse.com>
|
||||
|
||||
- CVE-2026-28348: improper keywords checking can allow external CSS loading
|
||||
(bsc#1259378)
|
||||
* added CVE-2026-28348.patch
|
||||
- CVE-2026-28350: lack of base tag handling can allow the hijacking of the
|
||||
resolution of relative URLs (bsc#1259379)
|
||||
* added CVE-2026-28350.patch
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Fri Apr 11 20:57:19 UTC 2025 - Dirk Müller <dmueller@suse.com>
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ License: BSD-3-Clause
|
||||
Group: Development/Languages/Python
|
||||
URL: https://github.com/fedora-python/lxml_html_clean/
|
||||
Source: https://files.pythonhosted.org/packages/source/l/lxml-html-clean/lxml_html_clean-%{version}.tar.gz
|
||||
# PATCH-FIX-UPSTREAM CVE-2026-28348.patch bsc#1259378 gh#fedora-python/lxml_html_clean@2ef7326
|
||||
Patch1: CVE-2026-28348.patch
|
||||
# PATCH-FIX-UPSTREAM CVE-2026-28350.patch bsc#1259379 gh#fedora-python/lxml_html_clean@9c5612c
|
||||
Patch2: CVE-2026-28350.patch
|
||||
BuildRequires: %{python_module base >= 3.6}
|
||||
BuildRequires: %{python_module pip}
|
||||
BuildRequires: %{python_module setuptools >= 61.0}
|
||||
|
||||
Reference in New Issue
Block a user