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

Reply via email to