From 31301cdb95f40a2f597b054ef8547d57b9d75661 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jul 2026 09:17:02 -0700 Subject: [PATCH] #22675 Validate RSS feed entry link schemes to prevent javascript: XSS --- netbox/extras/tests/test_dashboard.py | 65 ++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/netbox/extras/tests/test_dashboard.py b/netbox/extras/tests/test_dashboard.py index 529ab5f9e..43e8e9562 100644 --- a/netbox/extras/tests/test_dashboard.py +++ b/netbox/extras/tests/test_dashboard.py @@ -1,6 +1,7 @@ +from django.core.cache import cache from django.test import RequestFactory, TestCase, tag -from extras.dashboard.widgets import ObjectListWidget +from extras.dashboard.widgets import ObjectListWidget, RSSFeedWidget from extras.templatetags.dashboard import render_widget @@ -49,6 +50,68 @@ class ObjectListWidgetTestCase(TestCase): self.assertTrue('Unable to load content. Could not resolve list URL for:' in rendered) +class RSSFeedWidgetSanitizationTestCase(TestCase): + """ + Feed entry content is externally controlled and untrusted. Links must be validated against + ALLOWED_URL_SCHEMES so dangerous schemes (e.g. javascript:) cannot become clickable XSS sinks. + """ + + @tag('regression') + def test_sanitize_entries_blanks_disallowed_schemes(self): + entries = [ + {'link': 'javascript:alert(document.cookie)', 'title': 't1'}, + {'link': 'JavaScript:alert(1)', 'title': 't2'}, # case-insensitive + {'link': 'data:text/html,', 'title': 't3'}, + {'link': 'vbscript:msgbox(1)', 'title': 't4'}, + ] + RSSFeedWidget.sanitize_entries(entries) + for entry in entries: + self.assertEqual(entry['link'], '', msg=f"Failed to blank {entry['title']}") + + @tag('regression') + def test_sanitize_entries_preserves_allowed_links(self): + entries = [ + {'link': 'https://example.com/post', 'title': 't1'}, + {'link': 'http://example.com/post', 'title': 't2'}, + {'link': 'mailto:user@example.com', 'title': 't3'}, + {'link': '/relative/path', 'title': 't4'}, # schemeless relative link + ] + expected = [e['link'] for e in entries] + RSSFeedWidget.sanitize_entries(entries) + self.assertEqual([e['link'] for e in entries], expected) + + @tag('regression') + def test_sanitize_entries_cleans_summary_html(self): + entries = [ + {'link': 'https://example.com', 'title': 't1', 'summary': 'ok'}, + ] + RSSFeedWidget.sanitize_entries(entries) + self.assertNotIn('