Przeglądaj źródła

Closes #4717: Introduce ALLOWED_URL_SCHEMES configuration parameter to mitigate dangerous hyperlinks

Jeremy Stretch 5 lat temu
rodzic
commit
5af2b3c2f5

+ 8 - 0
docs/configuration/optional-settings.md

@@ -13,6 +13,14 @@ ADMINS = [
 
 
 ---
 ---
 
 
+## ALLOWED_URL_SCHEMES
+
+Default: `('file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp')`
+
+A list of permitted URL schemes referenced when rendering links within NetBox. Note that only the schemes specified in this list will be accepted: If adding your own, be sure to replicate the entire default list as well (excluding those schemes which are not desirable).
+
+---
+
 ## BANNER_TOP
 ## BANNER_TOP
 
 
 ## BANNER_BOTTOM
 ## BANNER_BOTTOM

+ 1 - 0
docs/release-notes/version-2.8.md

@@ -5,6 +5,7 @@
 ### Enhancements
 ### Enhancements
 
 
 * [#4698](https://github.com/netbox-community/netbox/issues/4698) - Improve display of template code for object in admin UI
 * [#4698](https://github.com/netbox-community/netbox/issues/4698) - Improve display of template code for object in admin UI
+* [#4717](https://github.com/netbox-community/netbox/issues/4717) - Introduce `ALLOWED_URL_SCHEMES` configuration parameter to mitigate dangerous hyperlinks
 * [#4755](https://github.com/netbox-community/netbox/issues/4755) - Enable creation of rack reservations directly from navigation menu
 * [#4755](https://github.com/netbox-community/netbox/issues/4755) - Enable creation of rack reservations directly from navigation menu
 
 
 ### Bug Fixes
 ### Bug Fixes

+ 5 - 0
netbox/netbox/configuration.example.py

@@ -68,6 +68,11 @@ ADMINS = [
     # ['John Doe', 'jdoe@example.com'],
     # ['John Doe', 'jdoe@example.com'],
 ]
 ]
 
 
+# URL schemes that are allowed within links in NetBox
+ALLOWED_URL_SCHEMES = (
+    'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
+)
+
 # Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same
 # Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same
 # content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
 # content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
 BANNER_TOP = ''
 BANNER_TOP = ''

+ 3 - 0
netbox/netbox/settings.py

@@ -58,6 +58,9 @@ SECRET_KEY = getattr(configuration, 'SECRET_KEY')
 
 
 # Set optional parameters
 # Set optional parameters
 ADMINS = getattr(configuration, 'ADMINS', [])
 ADMINS = getattr(configuration, 'ADMINS', [])
+ALLOWED_URL_SCHEMES = getattr(configuration, 'ALLOWED_URL_SCHEMES', (
+    'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
+))
 BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', '')
 BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', '')
 BANNER_LOGIN = getattr(configuration, 'BANNER_LOGIN', '')
 BANNER_LOGIN = getattr(configuration, 'BANNER_LOGIN', '')
 BANNER_TOP = getattr(configuration, 'BANNER_TOP', '')
 BANNER_TOP = getattr(configuration, 'BANNER_TOP', '')

+ 2 - 3
netbox/utilities/forms.py

@@ -647,9 +647,8 @@ class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultip
 
 
 class LaxURLField(forms.URLField):
 class LaxURLField(forms.URLField):
     """
     """
-    Modifies Django's built-in URLField in two ways:
-      1) Allow any valid scheme per RFC 3986 section 3.1
-      2) Remove the requirement for fully-qualified domain names (e.g. http://myserver/ is valid)
+    Modifies Django's built-in URLField to remove the requirement for fully-qualified domain names
+    (e.g. http://myserver/ is valid)
     """
     """
     default_validators = [EnhancedURLValidator()]
     default_validators = [EnhancedURLValidator()]
 
 

+ 5 - 1
netbox/utilities/templatetags/helpers.py

@@ -10,7 +10,6 @@ from django.utils.html import strip_tags
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 from markdown import markdown
 from markdown import markdown
 
 
-from utilities.choices import unpack_grouped_choices
 from utilities.utils import foreground_color
 from utilities.utils import foreground_color
 
 
 register = template.Library()
 register = template.Library()
@@ -39,6 +38,11 @@ def render_markdown(value):
     # Strip HTML tags
     # Strip HTML tags
     value = strip_tags(value)
     value = strip_tags(value)
 
 
+    # Sanitize Markdown links
+    schemes = '|'.join(settings.ALLOWED_URL_SCHEMES)
+    pattern = fr'\[(.+)\]\((?!({schemes})).*:(.+)\)'
+    value = re.sub(pattern, '[\\1](\\3)', value, flags=re.IGNORECASE)
+
     # Render Markdown
     # Render Markdown
     html = markdown(value, extensions=['fenced_code', 'tables'])
     html = markdown(value, extensions=['fenced_code', 'tables'])
 
 

+ 5 - 12
netbox/utilities/validators.py

@@ -1,31 +1,24 @@
 import re
 import re
 
 
+from django.conf import settings
 from django.core.validators import _lazy_re_compile, BaseValidator, URLValidator
 from django.core.validators import _lazy_re_compile, BaseValidator, URLValidator
 
 
 
 
 class EnhancedURLValidator(URLValidator):
 class EnhancedURLValidator(URLValidator):
     """
     """
-    Extends Django's built-in URLValidator to permit the use of hostnames with no domain extension.
+    Extends Django's built-in URLValidator to permit the use of hostnames with no domain extension and enforce allowed
+    schemes specified in the configuration.
     """
     """
-    class AnyURLScheme(object):
-        """
-        A fake URL list which "contains" all scheme names abiding by the syntax defined in RFC 3986 section 3.1
-        """
-        def __contains__(self, item):
-            if not item or not re.match(r'^[a-z][0-9a-z+\-.]*$', item.lower()):
-                return False
-            return True
-
     fqdn_re = URLValidator.hostname_re + URLValidator.domain_re + URLValidator.tld_re
     fqdn_re = URLValidator.hostname_re + URLValidator.domain_re + URLValidator.tld_re
     host_res = [URLValidator.ipv4_re, URLValidator.ipv6_re, fqdn_re, URLValidator.hostname_re]
     host_res = [URLValidator.ipv4_re, URLValidator.ipv6_re, fqdn_re, URLValidator.hostname_re]
     regex = _lazy_re_compile(
     regex = _lazy_re_compile(
-        r'^(?:[a-z0-9\.\-\+]*)://'          # Scheme (previously enforced by AnyURLScheme or schemes kwarg)
+        r'^(?:[a-z0-9\.\-\+]*)://'          # Scheme (enforced separately)
         r'(?:\S+(?::\S*)?@)?'               # HTTP basic authentication
         r'(?:\S+(?::\S*)?@)?'               # HTTP basic authentication
         r'(?:' + '|'.join(host_res) + ')'   # IPv4, IPv6, FQDN, or hostname
         r'(?:' + '|'.join(host_res) + ')'   # IPv4, IPv6, FQDN, or hostname
         r'(?::\d{2,5})?'                    # Port number
         r'(?::\d{2,5})?'                    # Port number
         r'(?:[/?#][^\s]*)?'                 # Path
         r'(?:[/?#][^\s]*)?'                 # Path
         r'\Z', re.IGNORECASE)
         r'\Z', re.IGNORECASE)
-    schemes = AnyURLScheme()
+    schemes = settings.ALLOWED_URL_SCHEMES
 
 
 
 
 class ExclusionValidator(BaseValidator):
 class ExclusionValidator(BaseValidator):