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