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>
|
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
|
Group: Development/Languages/Python
|
||||||
URL: https://github.com/fedora-python/lxml_html_clean/
|
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
|
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 base >= 3.6}
|
||||||
BuildRequires: %{python_module pip}
|
BuildRequires: %{python_module pip}
|
||||||
BuildRequires: %{python_module setuptools >= 61.0}
|
BuildRequires: %{python_module setuptools >= 61.0}
|
||||||
|
|||||||
Reference in New Issue
Block a user