Skip to content

Commit 7f1bba6

Browse files
committed
Check 'Host' header for local connections
1 parent b8b6633 commit 7f1bba6

File tree

3 files changed

+60
-0
lines changed

3 files changed

+60
-0
lines changed

notebook/base/handlers.py

Lines changed: 34 additions & 0 deletions
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,39 @@ 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("Blocking request with non-local 'Host' %s (%s)",
440+
host, self.request.host)
441+
return allow
442+
443+
def prepare(self):
444+
if not self.check_host():
445+
raise web.HTTPError(403)
446+
return super(IPythonHandler, self).prepare()
447+
414448
#---------------------------------------------------------------
415449
# template rendering
416450
#---------------------------------------------------------------

notebook/notebookapp.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager,
252252
password=jupyter_app.password,
253253
xsrf_cookies=True,
254254
disable_check_xsrf=jupyter_app.disable_check_xsrf,
255+
allow_remote_access=jupyter_app.allow_remote_access,
256+
local_hostnames=jupyter_app.local_hostnames,
255257

256258
# managers
257259
kernel_manager=kernel_manager,
@@ -831,6 +833,29 @@ def _token_changed(self, change):
831833
"""
832834
)
833835

836+
allow_remote_access = Bool(False, config=True,
837+
help="""Allow requests where the Host header doesn't point to a local server
838+
839+
By default, requests get a 403 forbidden response if the 'Host' header
840+
shows that the browser thinks it's on a non-local domain.
841+
Setting this option to True disables this check.
842+
843+
This protects against 'DNS rebinding' attacks, where a remote web server
844+
serves you a page and then changes its DNS to send later requests to a
845+
local IP, bypassing same-origin checks.
846+
847+
Local IP addresses (such as 127.0.0.1 and ::1) are allowed as local,
848+
along with hostnames configured in local_hostnames.
849+
""")
850+
851+
local_hostnames = List(Unicode(), ['localhost'], config=True,
852+
help="""Hostnames to allow as local when allow_remote_access is False.
853+
854+
Local IP addresses (such as 127.0.0.1 and ::1) are automatically accepted
855+
as local as well.
856+
"""
857+
)
858+
834859
open_browser = Bool(True, config=True,
835860
help="""Whether to open in a browser after starting.
836861
The specific browser used is platform dependent and

setup.py

Lines changed: 1 addition & 0 deletions
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)