--- Begin Message ---
Package: release.debian.org
Severity: normal
User: release.debian....@packages.debian.org
Usertags: unblock
Please unblock package jupyter-notebook, 5.7.4-2.1 -> 5.7.8-1 (pending
approval before the latter version is uploaded to unstable).
There are two new CVEs since 5.7.4:
* CVE-2019-9644 (#924515)
* CVE-2019-10255 (#925939)
The diff between 5.7.4 and 5.7.8 upstream consists mostly of fixes for
these issues. There are also a couple of small non-security related bug
fixes. In principle two of these fixes are not needed (one concerning
MIME types relevant only on Windows, one concerning compatibility with a
newer major version of tornado, which is not yet in debian), but it
seems preferable to use the upstream changes unmodified rather than
selectively remove a small fraction of them.
unblock jupyter-notebook/5.7.8-1
-- System Information:
Debian Release: buster/sid
APT prefers unstable
APT policy: (500, 'unstable')
Architecture: amd64 (x86_64)
Kernel: Linux 4.19.0-3-amd64 (SMP w/1 CPU core)
Kernel taint flags: TAINT_OOT_MODULE, TAINT_UNSIGNED_MODULE
Locale: LANG=en_GB.UTF-8, LC_CTYPE=en_GB.UTF-8 (charmap=UTF-8), LANGUAGE=en_GB
(charmap=UTF-8)
Shell: /bin/sh linked to /bin/dash
Init: systemd (via /run/systemd/system)
LSM: AppArmor: enabled
diff -Nru jupyter-notebook-5.7.4/debian/changelog
jupyter-notebook-5.7.8/debian/changelog
--- jupyter-notebook-5.7.4/debian/changelog 2019-03-30 14:52:25.000000000
+0000
+++ jupyter-notebook-5.7.8/debian/changelog 2019-04-07 11:46:04.000000000
+0000
@@ -1,3 +1,11 @@
+jupyter-notebook (5.7.8-1) unstable; urgency=medium
+
+ * New upstream release 5.7.8
+ * Fixes CVE-2019-9644 (Closes: #924515)
+ * Fixes CVE-CVE-2019-10255 (Closes: #925939)
+
+ -- Gordon Ball <gor...@chronitis.net> Sun, 07 Apr 2019 11:46:04 +0000
+
jupyter-notebook (5.7.4-2.1) unstable; urgency=medium
* Non-maintainer upload.
diff -Nru jupyter-notebook-5.7.4/docs/source/changelog.rst
jupyter-notebook-5.7.8/docs/source/changelog.rst
--- jupyter-notebook-5.7.4/docs/source/changelog.rst 2018-12-17
10:01:51.000000000 +0000
+++ jupyter-notebook-5.7.8/docs/source/changelog.rst 2019-04-01
10:22:11.000000000 +0000
@@ -21,6 +21,44 @@
Use ``pip install pip --upgrade`` to upgrade pip. Check pip version with
``pip --version``.
+.. _release-5.7.8:
+
+5.7.8
+-----
+
+- Fix regression in restarting kernels in 5.7.5.
+ The restart handler would return before restart was completed.
+- Further improve compatibility with tornado 6 with improved
+ checks for when websockets are closed.
+- Fix regression in 5.7.6 on Windows where .js files could have the wrong
mime-type.
+- Fix Open Redirect vulnerability (CVE-2019-10255)
+ where certain malicious URLs could redirect from the Jupyter login page
+ to a malicious site after a successful login.
+ 5.7.7 contained only a partial fix for this issue.
+
+.. _release-5.7.6:
+
+5.7.6
+-----
+
+5.7.6 contains a security fix for a cross-site inclusion (XSSI) vulnerability
(CVE-2019–9644),
+where files at a known URL could be included in a page from an unauthorized
website if the user is logged into a Jupyter server.
+The fix involves setting the ``X-Content-Type-Options: nosniff``
+header, and applying CSRF checks previously on all non-GET
+API requests to GET requests to API endpoints and the /files/ endpoint.
+
+The attacking page is able to access some contents of files when using
Internet Explorer through script errors,
+but this has not been demonstrated with other browsers.
+
+.. _release-5.7.5:
+
+5.7.5
+-----
+
+- Fix compatibility with tornado 6 (:ghpull:`4392`, :ghpull:`4449`).
+- Fix opening integer filedescriptor during startup on Python 2
(:ghpull:`4349`)
+- Fix compatibility with asynchronous `KernelManager.restart_kernel` methods
(:ghpull:`4412`)
+
.. _release-5.7.4:
5.7.4
diff -Nru jupyter-notebook-5.7.4/notebook/auth/login.py
jupyter-notebook-5.7.8/notebook/auth/login.py
--- jupyter-notebook-5.7.4/notebook/auth/login.py 2018-12-17
10:01:51.000000000 +0000
+++ jupyter-notebook-5.7.8/notebook/auth/login.py 2019-04-01
10:22:11.000000000 +0000
@@ -7,9 +7,9 @@
import os
try:
- from urllib.parse import urlparse # Py 3
+ from urllib.parse import urlparse, urlunparse # Py 3
except ImportError:
- from urlparse import urlparse # Py 2
+ from urlparse import urlparse, urlunparse # Py 2
import uuid
from tornado.escape import url_escape
@@ -39,15 +39,23 @@
"""
if default is None:
default = self.base_url
- if not url.startswith(self.base_url):
+ # protect chrome users from mishandling unescaped backslashes.
+ # \ is not valid in urls, but some browsers treat it as /
+ # instead of %5C, causing `\\` to behave as `//`
+ url = url.replace("\\", "%5C")
+ parsed = urlparse(url)
+ path_only = urlunparse(parsed._replace(netloc='', scheme=''))
+ if url != path_only or not (parsed.path +
'/').startswith(self.base_url):
# require that next_url be absolute path within our path
allow = False
# OR pass our cross-origin check
- if '://' in url:
+ if url != path_only:
# if full URL, run our cross-origin check:
- parsed = urlparse(url.lower())
origin = '%s://%s' % (parsed.scheme, parsed.netloc)
- if self.allow_origin:
+ origin = origin.lower()
+ if origin == '%s://%s' % (self.request.protocol,
self.request.host):
+ allow = True
+ elif self.allow_origin:
allow = self.allow_origin == origin
elif self.allow_origin_pat:
allow = bool(self.allow_origin_pat.match(origin))
diff -Nru jupyter-notebook-5.7.4/notebook/auth/tests/test_login.py
jupyter-notebook-5.7.8/notebook/auth/tests/test_login.py
--- jupyter-notebook-5.7.4/notebook/auth/tests/test_login.py 1970-01-01
00:00:00.000000000 +0000
+++ jupyter-notebook-5.7.8/notebook/auth/tests/test_login.py 2019-04-01
10:22:11.000000000 +0000
@@ -0,0 +1,54 @@
+"""Tests for login redirects"""
+
+import requests
+from tornado.httputil import url_concat
+
+from notebook.tests.launchnotebook import NotebookTestBase
+
+
+class LoginTest(NotebookTestBase):
+ def login(self, next):
+ first = requests.get(self.base_url() + "login")
+ first.raise_for_status()
+ resp = requests.post(
+ url_concat(
+ self.base_url() + "login",
+ {'next': next},
+ ),
+ allow_redirects=False,
+ data={
+ "password": self.token,
+ "_xsrf": first.cookies.get("_xsrf", ""),
+ },
+ cookies=first.cookies,
+ )
+ resp.raise_for_status()
+ return resp.headers['Location']
+
+ def test_next_bad(self):
+ for bad_next in (
+ "//some-host",
+ "//host" + self.url_prefix + "tree",
+ "https://google.com",
+ "/absolute/not/base_url",
+ "///jupyter.org",
+ "/\\some-host",
+ ):
+ url = self.login(next=bad_next)
+ self.assertEqual(url, self.url_prefix)
+ assert url
+
+ def test_next_ok(self):
+ for next_path in (
+ "tree/",
+ self.base_url() + "has/host",
+ "notebooks/notebook.ipynb",
+ "tree//something",
+ ):
+ if "://" in next_path:
+ expected = next_path
+ else:
+ expected = self.url_prefix + next_path
+
+ actual = self.login(next=expected)
+ self.assertEqual(actual, expected)
diff -Nru jupyter-notebook-5.7.4/notebook/base/handlers.py
jupyter-notebook-5.7.8/notebook/base/handlers.py
--- jupyter-notebook-5.7.4/notebook/base/handlers.py 2018-12-17
10:01:51.000000000 +0000
+++ jupyter-notebook-5.7.8/notebook/base/handlers.py 2019-04-01
10:22:11.000000000 +0000
@@ -82,6 +82,7 @@
def set_default_headers(self):
headers = {}
+ headers["X-Content-Type-Options"] = "nosniff"
headers.update(self.settings.get('headers', {}))
headers["Content-Security-Policy"] = self.content_security_policy
@@ -399,13 +400,69 @@
)
return allow
+ def check_referer(self):
+ """Check Referer for cross-site requests.
+
+ Disables requests to certain endpoints with
+ external or missing Referer.
+
+ If set, allow_origin settings are applied to the Referer
+ to whitelist specific cross-origin sites.
+
+ Used on GET for api endpoints and /files/
+ to block cross-site inclusion (XSSI).
+ """
+ host = self.request.headers.get("Host")
+ referer = self.request.headers.get("Referer")
+
+ if not host:
+ self.log.warning("Blocking request with no host")
+ return False
+ if not referer:
+ self.log.warning("Blocking request with no referer")
+ return False
+
+ referer_url = urlparse(referer)
+ referer_host = referer_url.netloc
+ if referer_host == host:
+ return True
+
+ # apply cross-origin checks to Referer:
+ origin = "{}://{}".format(referer_url.scheme, referer_url.netloc)
+ if self.allow_origin:
+ allow = self.allow_origin == origin
+ elif self.allow_origin_pat:
+ allow = bool(self.allow_origin_pat.match(origin))
+ else:
+ # No CORS settings, deny the request
+ allow = False
+
+ if not allow:
+ self.log.warning("Blocking Cross Origin request for %s. Referer:
%s, Host: %s",
+ self.request.path, origin, host,
+ )
+ return allow
+
def check_xsrf_cookie(self):
"""Bypass xsrf cookie checks when token-authenticated"""
if self.token_authenticated or self.settings.get('disable_check_xsrf',
False):
# Token-authenticated requests do not need additional XSRF-check
# Servers without authentication are vulnerable to XSRF
return
- return super(IPythonHandler, self).check_xsrf_cookie()
+ try:
+ return super(IPythonHandler, self).check_xsrf_cookie()
+ except web.HTTPError as e:
+ if self.request.method in {'GET', 'HEAD'}:
+ # Consider Referer a sufficient cross-origin check for GET
requests
+ if not self.check_referer():
+ referer = self.request.headers.get('Referer')
+ if referer:
+ msg = "Blocking Cross Origin request from
{}.".format(referer)
+ else:
+ msg = "Blocking request from unknown origin"
+ raise web.HTTPError(403, msg)
+ else:
+ raise
def check_host(self):
"""Check the host header if remote access disallowed.
@@ -650,13 +707,20 @@
"; sandbox allow-scripts"
@web.authenticated
+ def head(self, path):
+ self.check_xsrf_cookie()
+ return super(AuthenticatedFileHandler, self).head(path)
+
+ @web.authenticated
def get(self, path):
+ self.check_xsrf_cookie()
+
if os.path.splitext(path)[1] == '.ipynb' or
self.get_argument("download", False):
name = path.rsplit('/', 1)[-1]
self.set_attachment_header(name)
return web.StaticFileHandler.get(self, path)
-
+
def get_content_type(self):
path = self.absolute_path.strip('/')
if '/' in path:
diff -Nru jupyter-notebook-5.7.4/notebook/base/zmqhandlers.py
jupyter-notebook-5.7.8/notebook/base/zmqhandlers.py
--- jupyter-notebook-5.7.4/notebook/base/zmqhandlers.py 2018-12-17
10:01:51.000000000 +0000
+++ jupyter-notebook-5.7.8/notebook/base/zmqhandlers.py 2019-04-01
10:22:11.000000000 +0000
@@ -17,7 +17,8 @@
import tornado
from tornado import gen, ioloop, web
-from tornado.websocket import WebSocketHandler
+from tornado.iostream import StreamClosedError
+from tornado.websocket import WebSocketHandler, WebSocketClosedError
from jupyter_client.session import Session
from jupyter_client.jsonutil import date_default, extract_dates
@@ -172,7 +173,7 @@
def send_ping(self):
"""send a ping to keep the websocket alive"""
- if self.stream.closed() and self.ping_callback is not None:
+ if self.ws_connection is None and self.ping_callback is not None:
self.ping_callback.stop()
return
@@ -185,8 +186,13 @@
self.log.warning("WebSocket ping timeout after %i ms.",
since_last_pong)
self.close()
return
+ try:
+ self.ping(b'')
+ except (StreamClosedError, WebSocketClosedError):
+ # websocket has been closed, stop pinging
+ self.ping_callback.stop()
+ return
- self.ping(b'')
self.last_ping = now
def on_pong(self, data):
@@ -237,7 +243,7 @@
def _on_zmq_reply(self, stream, msg_list):
# Sometimes this gets triggered when the on_close method is scheduled
in the
# eventloop but hasn't been called.
- if self.stream.closed() or stream.closed():
+ if self.ws_connection is None or stream.closed():
self.log.warning("zmq message arrived on closed channel")
self.close()
return
@@ -246,8 +252,14 @@
msg = self._reserialize_reply(msg_list, channel=channel)
except Exception:
self.log.critical("Malformed message: %r" % msg_list,
exc_info=True)
- else:
+ return
+
+ try:
self.write_message(msg, binary=isinstance(msg, bytes))
+ except (StreamClosedError, WebSocketClosedError):
+ self.log.warning("zmq message arrived on closed channel")
+ self.close()
+ return
class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
@@ -281,7 +293,8 @@
# assign and yield in two step to avoid tornado 3 issues
res = self.pre_get()
yield gen.maybe_future(res)
- super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs)
+ res = super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs)
+ yield gen.maybe_future(res)
def initialize(self):
self.log.debug("Initializing websocket connection %s",
self.request.path)
diff -Nru jupyter-notebook-5.7.4/notebook/files/handlers.py
jupyter-notebook-5.7.8/notebook/files/handlers.py
--- jupyter-notebook-5.7.4/notebook/files/handlers.py 2018-12-17
10:01:51.000000000 +0000
+++ jupyter-notebook-5.7.8/notebook/files/handlers.py 2019-04-01
10:22:11.000000000 +0000
@@ -35,10 +35,13 @@
@web.authenticated
def head(self, path):
- self.get(path, include_body=False)
+ self.check_xsrf_cookie()
+ return self.get(path, include_body=False)
@web.authenticated
def get(self, path, include_body=True):
+ # /files/ requests must originate from the same site
+ self.check_xsrf_cookie()
cm = self.contents_manager
if cm.is_hidden(path) and not cm.allow_hidden:
diff -Nru jupyter-notebook-5.7.4/notebook/notebookapp.py
jupyter-notebook-5.7.8/notebook/notebookapp.py
--- jupyter-notebook-5.7.4/notebook/notebookapp.py 2018-12-17
10:01:51.000000000 +0000
+++ jupyter-notebook-5.7.8/notebook/notebookapp.py 2019-04-01
10:22:11.000000000 +0000
@@ -1581,10 +1581,12 @@
def init_mime_overrides(self):
# On some Windows machines, an application has registered an incorrect
- # mimetype for CSS in the registry. Tornado uses this when serving
- # .css files, causing browsers to reject the stylesheet. We know the
- # mimetype always needs to be text/css, so we override it here.
+ # mimetype for CSS and JavaScript in the registry.
+ # Tornado uses this when serving .css and .js files, causing browsers
to
+ # reject these files. We know the mimetype always needs to be text/css
for css
+ # and application/javascript for JS, so we override it here.
mimetypes.add_type('text/css', '.css')
+ mimetypes.add_type('application/javascript', '.js')
def shutdown_no_activity(self):
@@ -1739,7 +1741,7 @@
# Write a temporary file to open in the browser
fd, open_file = tempfile.mkstemp(suffix='.html')
- with open(fd, 'w', encoding='utf-8') as fh:
+ with io.open(fd, 'w', encoding='utf-8') as fh:
self._write_browser_open_file(uri, fh)
else:
open_file = self.browser_open_file
diff -Nru jupyter-notebook-5.7.4/notebook/services/kernels/kernelmanager.py
jupyter-notebook-5.7.8/notebook/services/kernels/kernelmanager.py
--- jupyter-notebook-5.7.4/notebook/services/kernels/kernelmanager.py
2018-12-17 10:01:51.000000000 +0000
+++ jupyter-notebook-5.7.8/notebook/services/kernels/kernelmanager.py
2019-04-01 10:22:11.000000000 +0000
@@ -280,10 +280,11 @@
self.last_kernel_activity = utcnow()
return super(MappingKernelManager, self).shutdown_kernel(kernel_id,
now=now)
+ @gen.coroutine
def restart_kernel(self, kernel_id):
"""Restart a kernel by kernel_id"""
self._check_kernel_id(kernel_id)
- super(MappingKernelManager, self).restart_kernel(kernel_id)
+ yield gen.maybe_future(super(MappingKernelManager,
self).restart_kernel(kernel_id))
kernel = self.get_kernel(kernel_id)
# return a Future that will resolve when the kernel has successfully
restarted
channel = kernel.connect_shell()
@@ -319,7 +320,8 @@
channel.on_recv(on_reply)
loop = IOLoop.current()
timeout = loop.add_timeout(loop.time() + self.kernel_info_timeout,
on_timeout)
- return future
+ # wait for restart to complete
+ yield future
def notify_connect(self, kernel_id):
"""Notice a new connection to a kernel"""
diff -Nru jupyter-notebook-5.7.4/notebook/services/nbconvert/handlers.py
jupyter-notebook-5.7.8/notebook/services/nbconvert/handlers.py
--- jupyter-notebook-5.7.4/notebook/services/nbconvert/handlers.py
2018-12-17 10:01:51.000000000 +0000
+++ jupyter-notebook-5.7.8/notebook/services/nbconvert/handlers.py
2019-04-01 10:22:11.000000000 +0000
@@ -9,6 +9,7 @@
@web.authenticated
def get(self):
+ self.check_xsrf_cookie()
try:
from nbconvert.exporters import base
except ImportError as e:
diff -Nru jupyter-notebook-5.7.4/notebook/static/base/js/namespace.js
jupyter-notebook-5.7.8/notebook/static/base/js/namespace.js
--- jupyter-notebook-5.7.4/notebook/static/base/js/namespace.js 2018-12-17
10:01:51.000000000 +0000
+++ jupyter-notebook-5.7.8/notebook/static/base/js/namespace.js 2019-04-01
10:22:11.000000000 +0000
@@ -73,7 +73,7 @@
// tree
jglobal('SessionList','tree/js/sessionlist');
- Jupyter.version = "5.7.4";
+ Jupyter.version = "5.7.8";
Jupyter._target = '_blank';
return Jupyter;
});
diff -Nru jupyter-notebook-5.7.4/notebook/utils.py
jupyter-notebook-5.7.8/notebook/utils.py
--- jupyter-notebook-5.7.4/notebook/utils.py 2018-12-17 10:01:51.000000000
+0000
+++ jupyter-notebook-5.7.8/notebook/utils.py 2019-04-01 10:22:11.000000000
+0000
@@ -13,12 +13,30 @@
from distutils.version import LooseVersion
try:
+ from inspect import isawaitable
+except ImportError:
+ def isawaitable(f):
+ """If isawaitable is undefined, nothing is awaitable"""
+ return False
+
+try:
+ from concurrent.futures import Future as ConcurrentFuture
+except ImportError:
+ class ConcurrentFuture:
+ """If concurrent.futures isn't importable, nothing will be a
c.f.Future"""
+ pass
+
+try:
from urllib.parse import quote, unquote, urlparse, urljoin
from urllib.request import pathname2url
except ImportError:
from urllib import quote, unquote, pathname2url
from urlparse import urlparse, urljoin
+# tornado.concurrent.Future is asyncio.Future
+# in tornado >=5 with Python 3
+from tornado.concurrent import Future as TornadoFuture
+from tornado import gen
from ipython_genutils import py3compat
# UF_HIDDEN is a stat flag not defined in the stat module.
@@ -306,3 +324,33 @@
check_pid = _check_pid_win32
else:
check_pid = _check_pid_posix
+
+
+def maybe_future(obj):
+ """Like tornado's gen.maybe_future
+
+ but more compatible with asyncio for recent versions
+ of tornado
+ """
+ if isinstance(obj, TornadoFuture):
+ return obj
+ elif isawaitable(obj):
+ return asyncio.ensure_future(obj)
+ elif isinstance(obj, ConcurrentFuture):
+ return asyncio.wrap_future(obj)
+ else:
+ # not awaitable, wrap scalar in future
+ f = TornadoFuture()
+ f.set_result(obj)
+ return f
+
+# monkeypatch tornado gen.maybe_future
+# on Python 3
+# TODO: remove monkeypatch after backporting smaller fix to 5.x
+try:
+ import asyncio
+except ImportError:
+ pass
+else:
+ import tornado.gen
+ tornado.gen.maybe_future = maybe_future
diff -Nru jupyter-notebook-5.7.4/notebook/_version.py
jupyter-notebook-5.7.8/notebook/_version.py
--- jupyter-notebook-5.7.4/notebook/_version.py 2018-12-17 10:01:51.000000000
+0000
+++ jupyter-notebook-5.7.8/notebook/_version.py 2019-04-01 10:22:11.000000000
+0000
@@ -9,5 +9,5 @@
# Next beta/alpha/rc release: The version number for beta is X.Y.ZbN **without
dots**.
-version_info = (5, 7, 4, '')
+version_info = (5, 7, 8)
__version__ = '.'.join(map(str, version_info[:3])) + ''.join(version_info[3:])
diff -Nru jupyter-notebook-5.7.4/setup.py jupyter-notebook-5.7.8/setup.py
--- jupyter-notebook-5.7.4/setup.py 2018-12-17 10:01:51.000000000 +0000
+++ jupyter-notebook-5.7.8/setup.py 2019-04-01 10:22:11.000000000 +0000
@@ -79,7 +79,7 @@
zip_safe = False,
install_requires = [
'jinja2',
- 'tornado>=4',
+ 'tornado>=4.1,<7',
# pyzmq>=17 is not technically necessary,
# but hopefully avoids incompatibilities with Tornado 5. April 2018
'pyzmq>=17',
diff -Nru jupyter-notebook-5.7.4/.travis.yml jupyter-notebook-5.7.8/.travis.yml
--- jupyter-notebook-5.7.4/.travis.yml 2018-12-17 10:01:51.000000000 +0000
+++ jupyter-notebook-5.7.8/.travis.yml 2019-04-01 10:22:11.000000000 +0000
@@ -49,7 +49,8 @@
fi
install:
- - pip install --pre .[test]
+ - pip install --pre .[test] $EXTRA_PIP
+ - pip freeze
- wget
https://github.com/jgm/pandoc/releases/download/1.19.1/pandoc-1.19.1-1-amd64.deb
&& sudo dpkg -i pandoc-1.19.1-1-amd64.deb
@@ -96,10 +97,19 @@
env: GROUP=python
- python: 3.5
env: GROUP=python
- - python: "3.7-dev"
+ - python: 3.7
+ dist: xenial
env: GROUP=python
- python: 3.6
env: GROUP=docs
+ - python: 3.6
+ env:
+ - GROUP=python
+ - EXTRA_PIP="tornado<5"
+ - python: 2.7
+ env:
+ - GROUP=python
+ - EXTRA_PIP="tornado<5"
after_success:
- codecov
--- End Message ---