|
@@ -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):
|