mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Closes #4717: Introduce ALLOWED_URL_SCHEMES configuration parameter to mitigate dangerous hyperlinks
This commit is contained in:
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 = ''
|
||||||
|
@ -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', '')
|
||||||
|
@ -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:
|
Modifies Django's built-in URLField to remove the requirement for fully-qualified domain names
|
||||||
1) Allow any valid scheme per RFC 3986 section 3.1
|
(e.g. http://myserver/ is valid)
|
||||||
2) Remove the requirement for fully-qualified domain names (e.g. http://myserver/ is valid)
|
|
||||||
"""
|
"""
|
||||||
default_validators = [EnhancedURLValidator()]
|
default_validators = [EnhancedURLValidator()]
|
||||||
|
|
||||||
|
@ -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'])
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
Reference in New Issue
Block a user