diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index fb8cbe566..18a35047a 100755 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -5,6 +5,7 @@ import datetime import functools +import ipaddress import json import mimetypes import os @@ -411,6 +412,39 @@ class IPythonHandler(AuthenticatedHandler): return return super(IPythonHandler, self).check_xsrf_cookie() + def check_host(self): + """Check the host header if remote access disallowed. + + Returns True if the request should continue, False otherwise. + """ + if self.settings.get('allow_remote_access', False): + return True + + # Remove port (e.g. ':8888') from host + host = re.match(r'^(.*?)(:\d+)?$', self.request.host).group(1) + + # Browsers format IPv6 addresses like [::1]; we need to remove the [] + if host.startswith('[') and host.endswith(']'): + host = host[1:-1] + + try: + addr = ipaddress.ip_address(host) + except ValueError: + # Not an IP address: check against hostnames + allow = host in self.settings.get('local_hostnames', []) + else: + allow = addr.is_loopback + + if not allow: + self.log.warning("Blocking request with non-local 'Host' %s (%s)", + host, self.request.host) + return allow + + def prepare(self): + if not self.check_host(): + raise web.HTTPError(403) + return super(IPythonHandler, self).prepare() + #--------------------------------------------------------------- # template rendering #--------------------------------------------------------------- diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index bbd462511..fa7499755 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -252,6 +252,8 @@ class NotebookWebApplication(web.Application): password=jupyter_app.password, xsrf_cookies=True, disable_check_xsrf=jupyter_app.disable_check_xsrf, + allow_remote_access=jupyter_app.allow_remote_access, + local_hostnames=jupyter_app.local_hostnames, # managers kernel_manager=kernel_manager, @@ -831,6 +833,29 @@ class NotebookApp(JupyterApp): """ ) + allow_remote_access = Bool(False, config=True, + help="""Allow requests where the Host header doesn't point to a local server + + By default, requests get a 403 forbidden response if the 'Host' header + shows that the browser thinks it's on a non-local domain. + Setting this option to True disables this check. + + This protects against 'DNS rebinding' attacks, where a remote web server + serves you a page and then changes its DNS to send later requests to a + local IP, bypassing same-origin checks. + + Local IP addresses (such as 127.0.0.1 and ::1) are allowed as local, + along with hostnames configured in local_hostnames. + """) + + local_hostnames = List(Unicode(), ['localhost'], config=True, + help="""Hostnames to allow as local when allow_remote_access is False. + + Local IP addresses (such as 127.0.0.1 and ::1) are automatically accepted + as local as well. + """ + ) + open_browser = Bool(True, config=True, help="""Whether to open in a browser after starting. The specific browser used is platform dependent and diff --git a/setup.py b/setup.py index 786781141..f6b18bfe6 100755 --- a/setup.py +++ b/setup.py @@ -94,6 +94,7 @@ for more information. 'prometheus_client' ], extras_require = { + ':python_version == "2.7"': ['ipaddress'], 'test:python_version == "2.7"': ['mock'], 'test': ['nose', 'coverage', 'requests', 'nose_warnings_filters', 'nbval', 'nose-exclude'],