diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 3c43929157b..119e6abf78a 100644 --- a/docs/configuration/optional-settings.md +++ b/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_BOTTOM diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md index 10a4e33f5be..33da13bcbdd 100644 --- a/docs/release-notes/version-2.8.md +++ b/docs/release-notes/version-2.8.md @@ -5,6 +5,7 @@ ### Enhancements * [#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 ### Bug Fixes diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 941cbcd8896..ae6a9099747 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -68,6 +68,11 @@ # ['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 # content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. BANNER_TOP = '' diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 92c4a0cad4b..181a2454b42 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -58,6 +58,9 @@ # Set optional parameters 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_LOGIN = getattr(configuration, 'BANNER_LOGIN', '') BANNER_TOP = getattr(configuration, 'BANNER_TOP', '') diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 872b52998e4..0cc928e83d8 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -647,9 +647,8 @@ def _get_initial_value(self, initial_data, field_name): 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()] diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 8a82fc48b3d..a70e917d8b0 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -10,7 +10,6 @@ from django.utils.safestring import mark_safe from markdown import markdown -from utilities.choices import unpack_grouped_choices from utilities.utils import foreground_color register = template.Library() @@ -39,6 +38,11 @@ def render_markdown(value): # Strip HTML tags 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 html = markdown(value, extensions=['fenced_code', 'tables']) diff --git a/netbox/utilities/validators.py b/netbox/utilities/validators.py index 3b08733cd5d..517a567a93f 100644 --- a/netbox/utilities/validators.py +++ b/netbox/utilities/validators.py @@ -1,31 +1,24 @@ import re +from django.conf import settings from django.core.validators import _lazy_re_compile, BaseValidator, 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 host_res = [URLValidator.ipv4_re, URLValidator.ipv6_re, fqdn_re, URLValidator.hostname_re] 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'(?:' + '|'.join(host_res) + ')' # IPv4, IPv6, FQDN, or hostname r'(?::\d{2,5})?' # Port number r'(?:[/?#][^\s]*)?' # Path r'\Z', re.IGNORECASE) - schemes = AnyURLScheme() + schemes = settings.ALLOWED_URL_SCHEMES class ExclusionValidator(BaseValidator):