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

This commit is contained in:
Arthur 2026-07-14 09:13:29 -07:00
parent ebee3578b9
commit 9cf75b60c1
1 changed files with 28 additions and 2 deletions

View File

@ -2,7 +2,7 @@ import logging
import uuid
from functools import cached_property
from hashlib import sha256
from urllib.parse import urlencode
from urllib.parse import urlencode, urlparse
import feedparser
import requests
@ -16,6 +16,8 @@ from django.utils.translation import gettext as _
from core.models import ObjectType
from extras.choices import BookmarkOrderingChoices
from netbox.config import get_config
from utilities.html import clean_html
from utilities.object_types import object_type_identifier, object_type_name
from utilities.permissions import get_permission_for_model
from utilities.proxy import resolve_proxies
@ -366,8 +368,12 @@ class RSSFeedWidget(DashboardWidget):
# Fetch RSS content from cache if available
if feed_content := cache.get(self.cache_key):
feed = feedparser.FeedParserDict(feed_content)
# Sanitize on read as well: cached content may have been written by a pre-fix
# release (during an upgrade) and thus not yet sanitized. Idempotent.
self.sanitize_entries(feed.get('entries', []))
return {
'feed': feedparser.FeedParserDict(feed_content),
'feed': feed,
}
# Fetch feed content from remote server
@ -390,6 +396,8 @@ class RSSFeedWidget(DashboardWidget):
# Cap number of entries
max_entries = self.config.get('max_entries')
feed['entries'] = feed['entries'][:max_entries]
# Sanitize feed-controlled content before caching/rendering
self.sanitize_entries(feed['entries'])
# Cache the feed content
cache.set(self.cache_key, dict(feed), self.config.get('cache_timeout'))
@ -397,6 +405,24 @@ class RSSFeedWidget(DashboardWidget):
'feed': feed,
}
@staticmethod
def sanitize_entries(entries):
"""
Sanitize feed-controlled entry content in place. The feed URL is untrusted external
content, so we must guard against dangerous URL schemes (e.g. javascript:) in entry
links and sanitize entry summaries as defense-in-depth.
"""
allowed_schemes = get_config().ALLOWED_URL_SCHEMES
for entry in entries:
# Blank any link whose scheme isn't permitted (blocks javascript:, data:, etc.)
if link := entry.get('link'):
result = urlparse(link)
if result.scheme and result.scheme.lower() not in allowed_schemes:
entry['link'] = ''
# Sanitize the summary HTML
if summary := entry.get('summary'):
entry['summary'] = clean_html(summary, allowed_schemes)
@register_widget
class BookmarksWidget(DashboardWidget):