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('