Browse Source

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

Arthur 1 week ago
parent
commit
9cf75b60c1
1 changed files with 28 additions and 2 deletions
  1. 28 2
      netbox/extras/dashboard/widgets.py

+ 28 - 2
netbox/extras/dashboard/widgets.py

@@ -2,7 +2,7 @@ import logging
 import uuid
 import uuid
 from functools import cached_property
 from functools import cached_property
 from hashlib import sha256
 from hashlib import sha256
-from urllib.parse import urlencode
+from urllib.parse import urlencode, urlparse
 
 
 import feedparser
 import feedparser
 import requests
 import requests
@@ -16,6 +16,8 @@ from django.utils.translation import gettext as _
 
 
 from core.models import ObjectType
 from core.models import ObjectType
 from extras.choices import BookmarkOrderingChoices
 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.object_types import object_type_identifier, object_type_name
 from utilities.permissions import get_permission_for_model
 from utilities.permissions import get_permission_for_model
 from utilities.proxy import resolve_proxies
 from utilities.proxy import resolve_proxies
@@ -366,8 +368,12 @@ class RSSFeedWidget(DashboardWidget):
 
 
         # Fetch RSS content from cache if available
         # Fetch RSS content from cache if available
         if feed_content := cache.get(self.cache_key):
         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 {
             return {
-                'feed': feedparser.FeedParserDict(feed_content),
+                'feed': feed,
             }
             }
 
 
         # Fetch feed content from remote server
         # Fetch feed content from remote server
@@ -390,6 +396,8 @@ class RSSFeedWidget(DashboardWidget):
             # Cap number of entries
             # Cap number of entries
             max_entries = self.config.get('max_entries')
             max_entries = self.config.get('max_entries')
             feed['entries'] = feed['entries'][: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 the feed content
             cache.set(self.cache_key, dict(feed), self.config.get('cache_timeout'))
             cache.set(self.cache_key, dict(feed), self.config.get('cache_timeout'))
 
 
@@ -397,6 +405,24 @@ class RSSFeedWidget(DashboardWidget):
             'feed': feed,
             '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
 @register_widget
 class BookmarksWidget(DashboardWidget):
 class BookmarksWidget(DashboardWidget):