From 6a5e22e646ac197c03e0325c6105fdbb8b297fc0 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sun, 11 Dec 2016 18:20:31 +0100 Subject: [PATCH 001/408] Use attachment:path instead of attachment://path This partly reverts #1659, see also #1655. --- notebook/static/notebook/js/textcell.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/notebook/static/notebook/js/textcell.js b/notebook/static/notebook/js/textcell.js index d063993d7..311775b23 100644 --- a/notebook/static/notebook/js/textcell.js +++ b/notebook/static/notebook/js/textcell.js @@ -247,9 +247,9 @@ define([ marked(text, function (err, html) { html = security.sanitize_html(html); html = $($.parseHTML(html)); - html.find('img[src^="attachment://"]').each(function (i, h) { + html.find('img[src^="attachment:"]').each(function (i, h) { h = $(h); - var key = h.attr('src').replace(/^attachment:\/\//, ''); + var key = h.attr('src').replace(/^attachment:/, ''); if (key in that.attachments) { data.attachments[key] = JSON.parse(JSON.stringify( that.attachments[key])); @@ -359,7 +359,7 @@ define([ 'type (' + d[0] + ')'); } that.add_attachment(key, blob.type, d[1]); - var img_md = '![' + key + '](attachment://' + key + ')'; + var img_md = '![' + key + '](attachment:' + key + ')'; that.code_mirror.replaceRange(img_md, pos); } reader.readAsDataURL(blob); @@ -406,9 +406,9 @@ define([ html.find("a[href]").not('[href^="#"]').attr("target", "_blank"); // replace attachment: by the corresponding entry // in the cell's attachments - html.find('img[src^="attachment://"]').each(function (i, h) { + html.find('img[src^="attachment:"]').each(function (i, h) { h = $(h); - var key = h.attr('src').replace(/^attachment:\/\//, ''); + var key = h.attr('src').replace(/^attachment:/, ''); if (key in that.attachments) { var att = that.attachments[key]; From a33ad66460782911fc304ead4c75d03b582a41bd Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 13 Dec 2016 13:07:14 +0100 Subject: [PATCH 002/408] clarify the default token behavior in help output --- notebook/notebookapp.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 88be0fba9..783737b5a 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -576,11 +576,12 @@ class NotebookApp(JupyterApp): self.cookie_secret_file ) - token = Unicode( + token = Unicode('', help="""Token used for authenticating first-time connections to the server. - - Only used when no password is enabled. - + + When no password is enabled, + the default is to generate a new, random token. + Setting to an empty string disables authentication altogether, which is NOT RECOMMENDED. """ ).tag(config=True) From 7c7f065547fd0e33a11d47dcf08e92ecf42ccb01 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 13 Dec 2016 13:07:28 +0100 Subject: [PATCH 003/408] include token info on login page --- notebook/templates/login.html | 40 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/notebook/templates/login.html b/notebook/templates/login.html index c3b2bef72..1cf0a3fad 100644 --- a/notebook/templates/login.html +++ b/notebook/templates/login.html @@ -20,7 +20,7 @@ - {% elif login_token_available %} -
-

- This notebook server has no password set, - but token-authentication is enabled. - - You need to open the notebook server with its first-time login token in the URL, - or enable a password in order to gain access. - The command: -

-
jupyter notebook list
-

- will show you the URLs of running servers with their tokens, - which you can copy and paste into your browser. -

-
{% else %}

No login available, you shouldn't be seeing this page.

{% endif %} @@ -58,6 +42,28 @@ {% endfor %} {% endif %} + {% block token_message %} +
+

+ If this notebook server has no password set, token authentication is enabled. + + You need to open the notebook server with its first-time login token in the URL, + or enable a password in order to gain access. + The command: +

+
jupyter notebook list
+

+ will show you the URLs of running servers with their tokens, + which you can copy and paste into your browser. For example: +

+
Currently running servers:
+http://localhost:8888/?token=c8de56fa... :: /Users/you/notebooks
+
+

+ Or you can paste just the token value into the password field on this page. +

+
+ {% endblock token_message %} {% endblock %} From 7fa5d5a1be147e9c8e14f61a2f4b3c0db1e2c00b Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 13 Dec 2016 13:09:55 +0100 Subject: [PATCH 004/408] cover token authentication in security docs --- docs/source/changelog.rst | 6 ++-- docs/source/security.rst | 74 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 8b1bf6100..dcf51b132 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -15,14 +15,14 @@ For more detailed information, see .. _release-4.3: 4.3 ------ +--- 4.3 is a minor release with many bug fixes and improvements. Highlights: - API for creating mime-type based renderer extensions using :code:`OutputArea.register_mime_type` and :code:`Notebook.render_cell_output` methods. See `mimerender-cookiecutter `__ for reference implementations and cookiecutter. -- Enable token authentication by default +- Enable token authentication by default. See :ref:`server_security` for more details. - Update security docs to reflect new signature system - Switched from term.js to xterm.js @@ -31,7 +31,7 @@ Bug fixes: - Ensure variable is set if exc_info is falsey - Catch and log handler exceptions in :code:`events.trigger` - Add debug log for static file paths -- Don't check origin on token-authenticated requests +- Don't check origin on token-authenticated requests - Remove leftover print statement - Fix highlighting of Python code blocks - :code:`json_errors` should be outermost decorator on API handlers diff --git a/docs/source/security.rst b/docs/source/security.rst index eb1e039e4..491270789 100644 --- a/docs/source/security.rst +++ b/docs/source/security.rst @@ -1,7 +1,77 @@ + +.. _server_security: + +Security in the Jupyter notebook server +======================================= + +Since access to the Jupyter notebook server means access to running arbitrary code, +it is important to restrict access to the notebook server. +For this reason, notebook 4.3 introduces token-based authentication that is **on by default**. + +.. note:: + + If you enable a password for your notebook server, + token authentication is not enabled by default, + and the behavior of the notebook server is unchanged from from versions earlier than 4.3. + +When token authentication is enabled, the notebook uses a token to authenticate requests. +This token can be provided to login to the notebook server in three ways: + +- in the ``Authorization`` header, e.g.:: + + Authorization: token abcdef... + +- In a URL parameter, e.g.:: + + https://my-notebook/tree/?token=abcdef... + +- In the password field of the login form that will be shown to you if you are not logged in. + +When you start a notebook server with token authentication enabled (default), +a token is generated to use for authentication. +This token is logged to the terminal, so that you can copy/paste the URL into your browser:: + + [I 11:59:16.597 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/?token=c8de56fa4deed24899803e93c227592aef6538f93025fe01 + + +If the notebook server is going to open your browser automatically +(the default, unless ``--no-browser`` has been passed), +an *additional* token is generated for launching the browser. +This additional token can be used only once, +and is used to set a cookie for your browser once it connects. +After your browser has made its first request with this one-time-token, +the token is discarded and a cookie is set in your browser. + +At any later time, you can see the tokens and URLs for all of your running servers with :command:`jupyter notebook list`:: + + $ jupyter notebook list + Currently running servers: + http://localhost:8888/?token=abc... :: /home/you/notebooks + https://0.0.0.0:9999/?token=123... :: /tmp/public + http://localhost:8889/ :: /tmp/has-password + +For servers with token-authentication enabled, the URL in the above listing will include the token, +so you can copy and paste that URL into your browser to login. +If a server has no token (e.g. it has a password or has authentication disabled), +the URL will not include the token argument. +Once you have visited this URL, +a cookie will be set in your browser and you won't need to use the token again, +unless you switch browsers, clear your cookies, or start a notebook server on a new port. + + +You can disable authentication altogether by setting the token and password to empty strings, +but this is **NOT RECOMMENDED**, unless authentication or access restrictions are handled at a different layer in your web application: + +.. sourcecode:: python + + c.NotebookApp.token = '' + c.NotebookApp.password = '' + + .. _notebook_security: -Security in Jupyter notebooks -============================= +Security in notebook documents +============================== As Jupyter notebooks become more popular for sharing and collaboration, the potential for malicious people to attempt to exploit the notebook From a51efa5accb0d2d025b845fca54835bdecba28e2 Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 14 Dec 2016 10:44:30 +0100 Subject: [PATCH 005/408] add Authorization to allowed CORS headers so that CORS requests can be token-authenticated --- notebook/base/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index 3ab515c61..5ef320b13 100755 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -416,7 +416,7 @@ class APIHandler(IPythonHandler): return super(APIHandler, self).finish(*args, **kwargs) def options(self, *args, **kwargs): - self.set_header('Access-Control-Allow-Headers', 'accept, content-type') + self.set_header('Access-Control-Allow-Headers', 'accept, content-type, authorization') self.set_header('Access-Control-Allow-Methods', 'GET, PUT, POST, PATCH, DELETE, OPTIONS') self.finish() From a2f7325537fa7b92040e88856f29f309f823a22e Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 16 Dec 2016 13:50:42 +0100 Subject: [PATCH 006/408] fix carriage return handling The only real fix is an errant `+` on the final replacement, which would end up skipping sequential replacements. - leaves trailing `\r` on the text, if there is one - use groups to avoid unnecessary replace calls (no change) - includes test --- notebook/static/base/js/utils.js | 8 ++++---- notebook/tests/base/utils.js | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/notebook/static/base/js/utils.js b/notebook/static/base/js/utils.js index 66c1db918..46911233b 100644 --- a/notebook/static/base/js/utils.js +++ b/notebook/static/base/js/utils.js @@ -445,11 +445,11 @@ define([ // carriage return characters function fixCarriageReturn(txt) { txt = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline - while (txt.search(/\r/g) > -1) { - var base = txt.match(/^.*\r+/m)[0].replace(/\r/, ''); - var insert = txt.match(/\r+.*$/m)[0].replace(/\r/, ''); + while (txt.search(/\r[^$]/g) > -1) { + var base = txt.match(/^(.*)\r+/m)[1]; + var insert = txt.match(/\r+(.*)$/m)[1]; insert = insert + base.slice(insert.length, base.length); - txt = txt.replace(/\r+.*$/m, '\r').replace(/^.*\r+/m, insert); + txt = txt.replace(/\r+.*$/m, '\r').replace(/^.*\r/m, insert); } return txt; } diff --git a/notebook/tests/base/utils.js b/notebook/tests/base/utils.js index 0038e58cd..4d6428a68 100644 --- a/notebook/tests/base/utils.js +++ b/notebook/tests/base/utils.js @@ -48,7 +48,33 @@ casper.notebook_test(function () { that.test.assertEquals(result, testcase.result, "Overwriting characters processed"); }); + var input = [ + 'hasrn\r\n', + 'hasn\n', + '\n', + 'abcdef\r', + 'hello\n', + 'ab3\r', + 'x2\r\r', + '1\r', + ].join(''); + + var output = [ + 'hasrn\n', + 'hasn\n', + '\n', + 'hellof\n', + '123\r' + ].join(''); + + var result = this.evaluate(function (input) { + return IPython.utils.fixCarriageReturn(input); + }, input); + + this.test.assertEquals(result, output, "IPython.utils.fixCarriageReturns works"); + // Test load_extensions + this.thenEvaluate(function() { define('nbextensions/a', [], function() { window.a = true; }); define('nbextensions/c', [], function() { window.c = true; }); From e909afc93a328e91637082f07fd75f724adead33 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Sat, 17 Dec 2016 21:01:41 -0500 Subject: [PATCH 007/408] Due to jQuery propagating events, the window resize event could be triggered by a bubbled event. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://bugs.jquery.com/ticket/9841. In our case, the OutputArea was triggering a ‘resize’ event on its element, which was bubbling up and causing this handler to execute every time an output was appended. This was a pretty big drain on output areas that quickly changed (like for interact widgets), presumably since this function involves a DOM read to get heights. --- notebook/static/base/js/page.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/notebook/static/base/js/page.js b/notebook/static/base/js/page.js index bb522dcfe..fc7d4f137 100644 --- a/notebook/static/base/js/page.js +++ b/notebook/static/base/js/page.js @@ -52,9 +52,13 @@ define([ this._resize_site(); }; - Page.prototype._resize_site = function() { + Page.prototype._resize_site = function(e) { // Update the site's size. - $('div#site').height($(window).height() - $('#header').height()); + // only trigger if the event actually is the window's, not bubbling up. + // See https://bugs.jquery.com/ticket/9841#comment:8 + if (!e.target.tagName) { + $('div#site').height($(window).height() - $('#header').height()); + } }; return {'Page': Page}; From 3f32d7da002b727425db0ca28b626a5938374390 Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 19 Dec 2016 13:27:45 +0100 Subject: [PATCH 008/408] remove debug statement about no custom error page template We don't need a message when the default error page is used --- notebook/base/handlers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index 5ef320b13..ddc22b3e0 100755 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -383,15 +383,14 @@ class IPythonHandler(AuthenticatedHandler): message=message, exception=exception, ) - + self.set_header('Content-Type', 'text/html') # render the template try: html = self.render_template('%s.html' % status_code, **ns) except TemplateNotFound: - self.log.debug("No template for %d", status_code) html = self.render_template('error.html', **ns) - + self.write(html) From 3c0da28764714b0368e2ea3f3b5ec86a8779f4b9 Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 19 Dec 2016 14:39:40 +0100 Subject: [PATCH 009/408] add missing waits for output in display_id tests --- notebook/tests/notebook/display_id.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/notebook/tests/notebook/display_id.js b/notebook/tests/notebook/display_id.js index 7c31063c9..6df29f0c4 100644 --- a/notebook/tests/notebook/display_id.js +++ b/notebook/tests/notebook/display_id.js @@ -43,6 +43,7 @@ casper.notebook_test(function () { }); this.wait_for_output(1); + this.wait_for_idle() this.then(function () { var outputs = get_outputs(1); @@ -64,6 +65,7 @@ casper.notebook_test(function () { }); this.wait_for_output(2); + this.wait_for_idle(); this.then(function () { var outputs1 = get_outputs(1); @@ -108,6 +110,7 @@ casper.notebook_test(function () { kernel.output_callback_overrides_push(msg_id, callback_id); }); + this.wait_for_output(3); this.wait_for_idle(); this.then(function () { @@ -137,6 +140,7 @@ casper.notebook_test(function () { cell.execute(); }); + this.wait_for_output(4); this.wait_for_idle(); this.then(function () { From 935af4358281c380a2bbc903f93a5e0725cae420 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Mon, 19 Dec 2016 09:15:31 -0500 Subject: [PATCH 010/408] handle window sizing even when it is not from a resize event. --- notebook/static/base/js/page.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/notebook/static/base/js/page.js b/notebook/static/base/js/page.js index fc7d4f137..771ae38fa 100644 --- a/notebook/static/base/js/page.js +++ b/notebook/static/base/js/page.js @@ -52,11 +52,17 @@ define([ this._resize_site(); }; + + Page.prototype._resize_site = function(e) { - // Update the site's size. - // only trigger if the event actually is the window's, not bubbling up. - // See https://bugs.jquery.com/ticket/9841#comment:8 - if (!e.target.tagName) { + /** + * Update the site's size. + */ + + // In the case an event is passed in, only trigger if the event does + // *not* have a target DOM node (i.e., it is not bubbling up). See + // https://bugs.jquery.com/ticket/9841#comment:8 + if (!(e && e.target && e.target.tagName)) { $('div#site').height($(window).height() - $('#header').height()); } }; From faf60320dda6f08cbb2db5b4b8754de812ce6580 Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 19 Dec 2016 15:02:36 +0100 Subject: [PATCH 011/408] Further highlight token info in log output add critical-level log statement at the end of startup with token info --- notebook/notebookapp.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 783737b5a..39962634d 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -1321,7 +1321,15 @@ class NotebookApp(JupyterApp): b = lambda : browser.open(url_path_join(self.connection_url, uri), new=2) threading.Thread(target=b).start() - + + if self.token: + self.log.critical('\n'.join([ + '\n', + 'Copy/paste this URL into your browser when you connect for the first time,', + 'to login with a token:', + ' %s' % url_concat(self.connection_url, {'token': self.token}), + ])) + self.io_loop = ioloop.IOLoop.current() if sys.platform.startswith('win'): # add no-op to wake every 5s From 65cb6cc1859e545404dcac347d6bfcd2947b6295 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 20 Dec 2016 15:28:16 +0100 Subject: [PATCH 012/408] Only show token info if tokens are available on login page --- notebook/templates/login.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/notebook/templates/login.html b/notebook/templates/login.html index 1cf0a3fad..19a38418c 100644 --- a/notebook/templates/login.html +++ b/notebook/templates/login.html @@ -20,7 +20,7 @@ {% endblock token_message %} {% endif %} From 161c174a98ededfd11281101d4a8617ee9281315 Mon Sep 17 00:00:00 2001 From: Srinivas Reddy Thatiparthy Date: Wed, 21 Dec 2016 21:53:36 +0530 Subject: [PATCH 015/408] rename log.warn to log.warning as log.warn is deprecated --- notebook/auth/login.py | 2 +- notebook/notebookapp.py | 2 +- notebook/services/sessions/handlers.py | 4 ++-- setupbase.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/notebook/auth/login.py b/notebook/auth/login.py index 2484398ab..ee260b7da 100644 --- a/notebook/auth/login.py +++ b/notebook/auth/login.py @@ -52,7 +52,7 @@ class LoginHandler(IPythonHandler): allow = bool(self.allow_origin_pat.match(origin)) if not allow: # not allowed, use default - self.log.warn("Not allowing login redirect to %r" % url) + self.log.warning("Not allowing login redirect to %r" % url) url = default self.redirect(url) diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 8f9341954..7998c14d4 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -192,7 +192,7 @@ class NotebookWebApplication(web.Application): version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S") if jupyter_app.ignore_minified_js: - log.warn("""The `ignore_minified_js` flag is deprecated and no + log.warning("""The `ignore_minified_js` flag is deprecated and no longer works. Alternatively use `npm run build:watch` when working on the notebook's Javascript and LESS""") warnings.warn("The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0", DeprecationWarning) diff --git a/notebook/services/sessions/handlers.py b/notebook/services/sessions/handlers.py index 3a2c90490..2b3b677b0 100644 --- a/notebook/services/sessions/handlers.py +++ b/notebook/services/sessions/handlers.py @@ -41,7 +41,7 @@ class SessionRootHandler(APIHandler): raise web.HTTPError(400, "No JSON data provided") if 'notebook' in model and 'path' in model['notebook']: - self.log.warn('Sessions API changed, see updated swagger docs') + self.log.warning('Sessions API changed, see updated swagger docs') model['path'] = model['notebook']['path'] model['type'] = 'notebook' @@ -119,7 +119,7 @@ class SessionHandler(APIHandler): changes = {} if 'notebook' in model and 'path' in model['notebook']: - self.log.warn('Sessions API changed, see updated swagger docs') + self.log.warning('Sessions API changed, see updated swagger docs') model['path'] = model['notebook']['path'] model['type'] = 'notebook' if 'path' in model: diff --git a/setupbase.py b/setupbase.py index bc8b6c03a..ab704084e 100755 --- a/setupbase.py +++ b/setupbase.py @@ -521,11 +521,11 @@ def css_js_prerelease(command, strict=False): # die if strict or any targets didn't build prefix = os.path.commonprefix([repo_root + os.sep] + missing) missing = [ m[len(prefix):] for m in missing ] - log.warn("rebuilding js and css failed. The following required files are missing: %s" % missing) + log.warning("rebuilding js and css failed. The following required files are missing: %s" % missing) raise e else: - log.warn("rebuilding js and css failed (not a problem)") - log.warn(str(e)) + log.warning("rebuilding js and css failed (not a problem)") + log.warning(str(e)) # check again for missing targets, just in case: missing = [ t for t in targets if not os.path.exists(t) ] From 2113b461de6f166c567845731a1e0cacffabf1bc Mon Sep 17 00:00:00 2001 From: Grant Nestor Date: Wed, 21 Dec 2016 11:34:58 -0800 Subject: [PATCH 016/408] Add 4.3.1 to changelog --- docs/source/changelog.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index dcf51b132..7b7842e55 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -12,6 +12,29 @@ For more detailed information, see Use ``pip install notebook --upgrade`` or ``conda upgrade notebook`` to upgrade to the latest release. +.. _release-4.3.1: + +4.3.1 +----- + +4.3.1 is a patch release with a security patch, a couple bug fixes, and improvements to the newly-released token authentication. + +Bug fixes: + +- Fix carriage return handling +- Make the font size more robust against fickle brow +- Ignore resize events that bubbled up and didn't come from window + +Other improvements: + +- Better docs for token-based authentication +- Further highlight token info in log output when autogenerated +- Add Authorization to allowed CORS headers + +See the 4.3 milestone on GitHub for a complete list of +`issues `__ +and `pull requests `__ involved in this release. + .. _release-4.3: 4.3 From e03ab774607fb7b6b70d331f98680cb54f003e28 Mon Sep 17 00:00:00 2001 From: Grant Nestor Date: Wed, 21 Dec 2016 12:52:06 -0800 Subject: [PATCH 017/408] Downgrade to CodeMirror 5.16 Closes https://github.com/jupyter/notebook/issues/1967 --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 267a7a880..bb5e83ee5 100644 --- a/bower.json +++ b/bower.json @@ -5,7 +5,7 @@ "backbone": "components/backbone#~1.2", "bootstrap": "components/bootstrap#~3.3", "bootstrap-tour": "0.9.0", - "codemirror": "components/codemirror#~5.21", + "codemirror": "components/codemirror#~5.16", "font-awesome": "components/font-awesome#~4.2.0", "google-caja": "5669", "jquery": "components/jquery#~2.0", From 4a8af93b5bd92c02a2502cdc7281a9e7b90ac780 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 13 Dec 2016 14:57:57 +0100 Subject: [PATCH 018/408] enable tornado xsrf cookie --- notebook/base/handlers.py | 19 +++++++++++++++++-- notebook/notebookapp.py | 1 + notebook/templates/login.html | 1 + 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index 5ef320b13..88c882ecc 100755 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -288,8 +288,12 @@ class IPythonHandler(AuthenticatedHandler): host = self.request.headers.get("Host") origin = self.request.headers.get("Origin") - # If no header is provided, assume it comes from a script/curl. - # We are only concerned with cross-site browser stuff here. + # If no header is provided, allow it. + # Origin can be None for: + # - same-origin (IE, Firefox) + # - Cross-site POST form (IE, Firefox) + # - Scripts + # The cross-site POST (XSRF) case is handled by tornado's xsrf_token if origin is None or host is None: return True @@ -340,6 +344,8 @@ class IPythonHandler(AuthenticatedHandler): sys_info=sys_info, contents_js_source=self.contents_js_source, version_hash=self.version_hash, + ignore_minified_js=self.ignore_minified_js, + xsrf_form_html=self.xsrf_form_html, **self.jinja_template_vars ) @@ -403,6 +409,15 @@ class APIHandler(IPythonHandler): raise web.HTTPError(404) return super(APIHandler, self).prepare() + def check_xsrf_cookie(self): + """Check non-empty body on POST for XSRF + + instead of checking the cookie for forms. + """ + if self.request.method.upper() == 'POST' and not self.request.body: + # Require non-empty POST body for XSRF + raise web.HTTPError(400, "POST requests must have a JSON body. If no content is needed, use '{}'.") + @property def content_security_policy(self): csp = '; '.join([ diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 7998c14d4..484ba7e2e 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -225,6 +225,7 @@ class NotebookWebApplication(web.Application): login_handler_class=jupyter_app.login_handler_class, logout_handler_class=jupyter_app.logout_handler_class, password=jupyter_app.password, + xsrf_cookies=True, # managers kernel_manager=kernel_manager, diff --git a/notebook/templates/login.html b/notebook/templates/login.html index df394db32..61aafaf17 100644 --- a/notebook/templates/login.html +++ b/notebook/templates/login.html @@ -22,6 +22,7 @@
+ {{ xsrf_form_html() | safe }} From 70e79a0ad69f02d46abed57e007ce5ef7f79319b Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 13 Dec 2016 15:54:41 +0100 Subject: [PATCH 019/408] add token_authenticated property indicates if a token is used for authentication, in which case xsrf checks should be skipped. --- notebook/auth/login.py | 78 +++++++++++++++++++++++++-------------- notebook/base/handlers.py | 30 +++++++++------ 2 files changed, 69 insertions(+), 39 deletions(-) diff --git a/notebook/auth/login.py b/notebook/auth/login.py index ee260b7da..3511058ef 100644 --- a/notebook/auth/login.py +++ b/notebook/auth/login.py @@ -97,7 +97,7 @@ class LoginHandler(IPythonHandler): auth_header_pat = re.compile('token\s+(.+)', re.IGNORECASE) @classmethod - def get_user_token(cls, handler): + def get_token(cls, handler): """Get the user token from a request Default: @@ -120,11 +120,22 @@ class LoginHandler(IPythonHandler): Origin check should be skipped for token-authenticated requests. """ + return not cls.is_token_authenticated(handler) + + @classmethod + def is_token_authenticated(cls, handler): + """Check if the handler has been authenticated by a token. + + This is used to signal certain things, such as: + + - permit access to REST API + - xsrf protection + - skip origin-checks for scripts + """ if getattr(handler, '_user_id', None) is None: # ensure get_user has been called, so we know if we're token-authenticated handler.get_current_user() - token_authenticated = getattr(handler, '_token_authenticated', False) - return not token_authenticated + return getattr(handler, '_token_authenticated', False) @classmethod def get_user(cls, handler): @@ -136,40 +147,51 @@ class LoginHandler(IPythonHandler): # called on LoginHandler itself. if getattr(handler, '_user_id', None): return handler._user_id - user_id = handler.get_secure_cookie(handler.cookie_name) - if not user_id: + user_id = cls.get_user_token(handler) + if user_id is None: + user_id = handler.get_secure_cookie(handler.cookie_name) + else: + cls.set_login_cookie(handler, user_id) + # Record that we've been authenticated with a token. + # Used in should_check_origin above. + handler._token_authenticated = True + if user_id is None: # prevent extra Invalid cookie sig warnings: handler.clear_login_cookie() - token = handler.token - if not token and not handler.login_available: + if not handler.login_available: # Completely insecure! No authentication at all. # No need to warn here, though; validate_security will have already done that. - return 'anonymous' - if token: - # check login token from URL argument or Authorization header - user_token = cls.get_user_token(handler) - one_time_token = handler.one_time_token - authenticated = False - if user_token == token: - # token-authenticated, set the login cookie - handler.log.info("Accepting token-authenticated connection from %s", handler.request.remote_ip) - authenticated = True - elif one_time_token and user_token == one_time_token: - # one-time-token-authenticated, only allow this token once - handler.settings.pop('one_time_token', None) - handler.log.info("Accepting one-time-token-authenticated connection from %s", handler.request.remote_ip) - authenticated = True - if authenticated: - user_id = uuid.uuid4().hex - cls.set_login_cookie(handler, user_id) - # Record that we've been authenticated with a token. - # Used in should_check_origin above. - handler._token_authenticated = True + user_id = 'anonymous' # cache value for future retrievals on the same request handler._user_id = user_id return user_id + @classmethod + def get_user_token(cls, handler): + """Identify the user based on a token in the URL or Authorization header""" + token = handler.token + if not token: + return + # check login token from URL argument or Authorization header + user_token = cls.get_token(handler) + one_time_token = handler.one_time_token + authenticated = False + if user_token == token: + # token-authenticated, set the login cookie + handler.log.debug("Accepting token-authenticated connection from %s", handler.request.remote_ip) + authenticated = True + elif one_time_token and user_token == one_time_token: + # one-time-token-authenticated, only allow this token once + handler.settings.pop('one_time_token', None) + handler.log.info("Accepting one-time-token-authenticated connection from %s", handler.request.remote_ip) + authenticated = True + + if authenticated: + return uuid.uuid4().hex + else: + return None + @classmethod def validate_security(cls, app, ssl_options=None): diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index 88c882ecc..7edc4099d 100755 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -48,7 +48,7 @@ def log(): class AuthenticatedHandler(web.RequestHandler): """A RequestHandler with an authenticated user.""" - + @property def content_security_policy(self): """The default Content-Security-Policy header @@ -95,6 +95,13 @@ class AuthenticatedHandler(web.RequestHandler): return False return not self.login_handler.should_check_origin(self) + @property + def token_authenticated(self): + """Have I been authenticated with a token?""" + if self.login_handler is None or not hasattr(self.login_handler, 'is_token_authenticated'): + return False + return self.login_handler.is_token_authenticated(self) + @property def cookie_name(self): default_cookie_name = non_alphanum.sub('-', 'username-{}'.format( @@ -317,7 +324,15 @@ class IPythonHandler(AuthenticatedHandler): self.request.path, origin, host, ) return allow - + + def check_xsrf_cookie(self): + """Bypass xsrf checks when token-authenticated""" + if self.token_authenticated: + # Token-authenticated requests do not need additional XSRF-check + # Servers without authentication are vulnerable to XSRF + return + return super(IPythonHandler, self).check_xsrf_cookie() + #--------------------------------------------------------------- # template rendering #--------------------------------------------------------------- @@ -346,6 +361,8 @@ class IPythonHandler(AuthenticatedHandler): version_hash=self.version_hash, ignore_minified_js=self.ignore_minified_js, xsrf_form_html=self.xsrf_form_html, + token=self.token, + xsrf_token=self.xsrf_token, **self.jinja_template_vars ) @@ -409,15 +426,6 @@ class APIHandler(IPythonHandler): raise web.HTTPError(404) return super(APIHandler, self).prepare() - def check_xsrf_cookie(self): - """Check non-empty body on POST for XSRF - - instead of checking the cookie for forms. - """ - if self.request.method.upper() == 'POST' and not self.request.body: - # Require non-empty POST body for XSRF - raise web.HTTPError(400, "POST requests must have a JSON body. If no content is needed, use '{}'.") - @property def content_security_policy(self): csp = '; '.join([ From 9478a6b82be4a21663c6ae2d85eb35722eab18b6 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 13 Dec 2016 16:58:58 +0100 Subject: [PATCH 020/408] use tornado xsrf token in API - Cookie-authenticated API requests must use set X-XSRFToken header - add utils.ajax for making ajax requests, adding xsrf header from default location --- notebook/base/handlers.py | 2 +- notebook/static/base/js/utils.js | 32 +++++++++++++++++--- notebook/static/services/kernels/kernel.js | 12 ++++---- notebook/static/services/sessions/session.js | 10 +++--- notebook/static/tree/js/notebooklist.js | 2 +- notebook/static/tree/js/sessionlist.js | 2 +- notebook/static/tree/js/terminallist.js | 6 ++-- notebook/templates/page.html | 9 +++++- 8 files changed, 53 insertions(+), 22 deletions(-) diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index 7edc4099d..1dd376438 100755 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -362,7 +362,7 @@ class IPythonHandler(AuthenticatedHandler): ignore_minified_js=self.ignore_minified_js, xsrf_form_html=self.xsrf_form_html, token=self.token, - xsrf_token=self.xsrf_token, + xsrf_token=self.xsrf_token.decode('utf8'), **self.jinja_template_vars ) diff --git a/notebook/static/base/js/utils.js b/notebook/static/base/js/utils.js index 46911233b..1089d40c6 100644 --- a/notebook/static/base/js/utils.js +++ b/notebook/static/base/js/utils.js @@ -603,7 +603,7 @@ define([ var to_absolute_cursor_pos = function (cm, cursor) { console.warn('`utils.to_absolute_cursor_pos(cm, pos)` is deprecated. Use `cm.indexFromPos(cursor)`'); - return cm.indexFromPos(cusrsor); + return cm.indexFromPos(cursor); }; var from_absolute_cursor_pos = function (cm, cursor_pos) { @@ -752,6 +752,29 @@ define([ return wrapped_error; }; + var ajax = function (url, settings) { + // like $.ajax, but ensure Authorization header is set + settings = _add_auth_header(settings); + return $.ajax(url, settings); + }; + + var _add_auth_header = function (settings) { + /** + * Adds auth header to jquery ajax settings + */ + settings = settings || {}; + if (!settings.headers) { + settings.headers = {}; + } + if (!settings.headers.Authorization) { + var xsrf_token = get_body_data('xsrfToken'); + if (xsrf_token) { + settings.headers['X-XSRFToken'] = xsrf_token; + } + } + return settings; + }; + var promising_ajax = function(url, settings) { /** * Like $.ajax, but returning an ES6 promise. success and error settings @@ -766,7 +789,7 @@ define([ log_ajax_error(jqXHR, status, error); reject(wrap_ajax_error(jqXHR, status, error)); }; - $.ajax(url, settings); + ajax(url, settings); }); }; @@ -1010,10 +1033,11 @@ define([ is_or_has : is_or_has, is_focused : is_focused, mergeopt: mergeopt, - ajax_error_msg : ajax_error_msg, - log_ajax_error : log_ajax_error, requireCodeMirrorMode : requireCodeMirrorMode, XHR_ERROR : XHR_ERROR, + ajax : ajax, + ajax_error_msg : ajax_error_msg, + log_ajax_error : log_ajax_error, wrap_ajax_error : wrap_ajax_error, promising_ajax : promising_ajax, WrappedError: WrappedError, diff --git a/notebook/static/services/kernels/kernel.js b/notebook/static/services/kernels/kernel.js index 1b609dd94..a5942d87d 100644 --- a/notebook/static/services/kernels/kernel.js +++ b/notebook/static/services/kernels/kernel.js @@ -152,7 +152,7 @@ define([ * @param {function} [error] - functon executed on ajax error */ Kernel.prototype.list = function (success, error) { - $.ajax(this.kernel_service_url, { + utils.ajax(this.kernel_service_url, { processData: false, cache: false, type: "GET", @@ -194,7 +194,7 @@ define([ } }; - $.ajax(url, { + utils.ajax(url, { processData: false, cache: false, type: "POST", @@ -218,7 +218,7 @@ define([ * @param {function} [error] - functon executed on ajax error */ Kernel.prototype.get_info = function (success, error) { - $.ajax(this.kernel_url, { + utils.ajax(this.kernel_url, { processData: false, cache: false, type: "GET", @@ -244,7 +244,7 @@ define([ Kernel.prototype.kill = function (success, error) { this.events.trigger('kernel_killed.Kernel', {kernel: this}); this._kernel_dead(); - $.ajax(this.kernel_url, { + utils.ajax(this.kernel_url, { processData: false, cache: false, type: "DELETE", @@ -278,7 +278,7 @@ define([ }; var url = utils.url_path_join(this.kernel_url, 'interrupt'); - $.ajax(url, { + utils.ajax(url, { processData: false, cache: false, type: "POST", @@ -323,7 +323,7 @@ define([ }; var url = utils.url_path_join(this.kernel_url, 'restart'); - $.ajax(url, { + utils.ajax(url, { processData: false, cache: false, type: "POST", diff --git a/notebook/static/services/sessions/session.js b/notebook/static/services/sessions/session.js index 4224c0ec1..ade904f1f 100644 --- a/notebook/static/services/sessions/session.js +++ b/notebook/static/services/sessions/session.js @@ -76,7 +76,7 @@ define([ * @param {function} [error] - functon executed on ajax error */ Session.prototype.list = function (success, error) { - $.ajax(this.session_service_url, { + utils.ajax(this.session_service_url, { processData: false, cache: false, type: "GET", @@ -117,7 +117,7 @@ define([ } }; - $.ajax(this.session_service_url, { + utils.ajax(this.session_service_url, { processData: false, cache: false, type: "POST", @@ -139,7 +139,7 @@ define([ * @param {function} [error] - functon executed on ajax error */ Session.prototype.get_info = function (success, error) { - $.ajax(this.session_url, { + utils.ajax(this.session_url, { processData: false, cache: false, type: "GET", @@ -165,7 +165,7 @@ define([ this.notebook_model.path = path; } - $.ajax(this.session_url, { + utils.ajax(this.session_url, { processData: false, cache: false, type: "PATCH", @@ -192,7 +192,7 @@ define([ this.kernel._kernel_dead(); } - $.ajax(this.session_url, { + utils.ajax(this.session_url, { processData: false, cache: false, type: "DELETE", diff --git a/notebook/static/tree/js/notebooklist.js b/notebook/static/tree/js/notebooklist.js index 4019e435a..dfe08b3e5 100644 --- a/notebook/static/tree/js/notebooklist.js +++ b/notebook/static/tree/js/notebooklist.js @@ -734,7 +734,7 @@ define([ 'api/sessions', encodeURIComponent(session.id) ); - $.ajax(url, settings); + utils.ajax(url, settings); } }; diff --git a/notebook/static/tree/js/sessionlist.js b/notebook/static/tree/js/sessionlist.js index 001eb9f04..3c664193b 100644 --- a/notebook/static/tree/js/sessionlist.js +++ b/notebook/static/tree/js/sessionlist.js @@ -62,7 +62,7 @@ define([ error : utils.log_ajax_error, }; var url = utils.url_path_join(this.base_url, 'api/sessions'); - $.ajax(url, settings); + utils.ajax(url, settings); }; SesssionList.prototype.sessions_loaded = function(data){ diff --git a/notebook/static/tree/js/terminallist.js b/notebook/static/tree/js/terminallist.js index c8e8fcf1c..975aad906 100644 --- a/notebook/static/tree/js/terminallist.js +++ b/notebook/static/tree/js/terminallist.js @@ -60,12 +60,12 @@ define([ this.base_url, 'api/terminals' ); - $.ajax(url, settings); + utils.ajax(url, settings); }; TerminalList.prototype.load_terminals = function() { var url = utils.url_path_join(this.base_url, 'api/terminals'); - $.ajax(url, { + utils.ajax(url, { type: "GET", cache: false, dataType: "json", @@ -113,7 +113,7 @@ define([ }; var url = utils.url_path_join(that.base_url, 'api/terminals', utils.encode_uri_components(name)); - $.ajax(url, settings); + utils.ajax(url, settings); return false; }); item.find(".item_buttons").text("").append(shutdown_button); diff --git a/notebook/templates/page.html b/notebook/templates/page.html index e96aac227..9da14665d 100644 --- a/notebook/templates/page.html +++ b/notebook/templates/page.html @@ -197,7 +197,14 @@ - +