Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b670fae

Browse files
authoredJul 3, 2018
Merge pull request #3714 from takluyver/check-host
Check 'Host' header for local connections
2 parents 1b69205 + 0d6ffa6 commit b670fae

File tree

3 files changed

+82
-0
lines changed

3 files changed

+82
-0
lines changed
 

‎notebook/base/handlers.py

+38
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import datetime
77
import functools
8+
import ipaddress
89
import json
910
import mimetypes
1011
import os
@@ -411,6 +412,43 @@ def check_xsrf_cookie(self):
411412
return
412413
return super(IPythonHandler, self).check_xsrf_cookie()
413414

415+
def check_host(self):
416+
"""Check the host header if remote access disallowed.
417+
418+
Returns True if the request should continue, False otherwise.
419+
"""
420+
if self.settings.get('allow_remote_access', False):
421+
return True
422+
423+
# Remove port (e.g. ':8888') from host
424+
host = re.match(r'^(.*?)(:\d+)?$', self.request.host).group(1)
425+
426+
# Browsers format IPv6 addresses like [::1]; we need to remove the []
427+
if host.startswith('[') and host.endswith(']'):
428+
host = host[1:-1]
429+
430+
try:
431+
addr = ipaddress.ip_address(host)
432+
except ValueError:
433+
# Not an IP address: check against hostnames
434+
allow = host in self.settings.get('local_hostnames', [])
435+
else:
436+
allow = addr.is_loopback
437+
438+
if not allow:
439+
self.log.warning(
440+
("Blocking request with non-local 'Host' %s (%s). "
441+
"If the notebook should be accessible at that name, "
442+
"set NotebookApp.allow_remote_access to disable the check."),
443+
host, self.request.host
444+
)
445+
return allow
446+
447+
def prepare(self):
448+
if not self.check_host():
449+
raise web.HTTPError(403)
450+
return super(IPythonHandler, self).prepare()
451+
414452
#---------------------------------------------------------------
415453
# template rendering
416454
#---------------------------------------------------------------

‎notebook/notebookapp.py

+43
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import hmac
1616
import importlib
1717
import io
18+
import ipaddress
1819
import json
1920
import logging
2021
import mimetypes
@@ -252,6 +253,8 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager,
252253
password=jupyter_app.password,
253254
xsrf_cookies=True,
254255
disable_check_xsrf=jupyter_app.disable_check_xsrf,
256+
allow_remote_access=jupyter_app.allow_remote_access,
257+
local_hostnames=jupyter_app.local_hostnames,
255258

256259
# managers
257260
kernel_manager=kernel_manager,
@@ -831,6 +834,46 @@ def _token_changed(self, change):
831834
"""
832835
)
833836

837+
allow_remote_access = Bool(config=True,
838+
help="""Allow requests where the Host header doesn't point to a local server
839+
840+
By default, requests get a 403 forbidden response if the 'Host' header
841+
shows that the browser thinks it's on a non-local domain.
842+
Setting this option to True disables this check.
843+
844+
This protects against 'DNS rebinding' attacks, where a remote web server
845+
serves you a page and then changes its DNS to send later requests to a
846+
local IP, bypassing same-origin checks.
847+
848+
Local IP addresses (such as 127.0.0.1 and ::1) are allowed as local,
849+
along with hostnames configured in local_hostnames.
850+
""")
851+
852+
@default('allow_remote_access')
853+
def _default_allow_remote(self):
854+
"""Disallow remote access if we're listening only on loopback addresses"""
855+
try:
856+
addr = ipaddress.ip_address(self.ip)
857+
except ValueError:
858+
# Address is a hostname
859+
for info in socket.getaddrinfo(self.ip, self.port, 0, socket.SOCK_STREAM):
860+
addr = info[4][0]
861+
if not py3compat.PY3:
862+
addr = addr.decode('ascii')
863+
if not ipaddress.ip_address(addr).is_loopback:
864+
return True
865+
return False
866+
else:
867+
return not addr.is_loopback
868+
869+
local_hostnames = List(Unicode(), ['localhost'], config=True,
870+
help="""Hostnames to allow as local when allow_remote_access is False.
871+
872+
Local IP addresses (such as 127.0.0.1 and ::1) are automatically accepted
873+
as local as well.
874+
"""
875+
)
876+
834877
open_browser = Bool(True, config=True,
835878
help="""Whether to open in a browser after starting.
836879
The specific browser used is platform dependent and

‎setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
'prometheus_client'
9595
],
9696
extras_require = {
97+
':python_version == "2.7"': ['ipaddress'],
9798
'test:python_version == "2.7"': ['mock'],
9899
'test': ['nose', 'coverage', 'requests', 'nose_warnings_filters',
99100
'nbval', 'nose-exclude'],

0 commit comments

Comments
 (0)
Please sign in to comment.