#22675 Validate RSS feed entry link schemes to prevent javascript: XSS

This commit is contained in:
Arthur 2026-07-14 09:17:02 -07:00
parent 9cf75b60c1
commit 31301cdb95
1 changed files with 64 additions and 1 deletions

View File

@ -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,<script>alert(1)</script>', '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': '<b>ok</b><script>alert(1)</script>'},
]
RSSFeedWidget.sanitize_entries(entries)
self.assertNotIn('<script>', entries[0]['summary'])
self.assertIn('<b>ok</b>', entries[0]['summary'])
@tag('regression')
def test_get_feed_sanitizes_cached_content(self):
"""
Content cached by a pre-fix release must be sanitized on read, not served verbatim.
"""
widget = RSSFeedWidget(config={
'feed_url': 'https://example.com/feed.xml',
'requires_internet': False,
'max_entries': 10,
})
# Simulate a poisoned feed left in the cache by an older release
cache.set(widget.cache_key, {
'bozo': False,
'entries': [
{'link': 'javascript:alert(1)', 'title': 'evil', 'summary': 'x'},
],
})
result = widget.get_feed()
self.assertEqual(result['feed']['entries'][0]['link'], '')
class RenderWidgetTemplateTagTestCase(TestCase):
def _make_context(self):