diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/notebooks/filenbmanager.py index d7153a3..78500ff 100644 --- a/IPython/html/services/notebooks/filenbmanager.py +++ b/IPython/html/services/notebooks/filenbmanager.py @@ -278,7 +278,7 @@ class FileNotebookManager(NotebookManager): nb = current.read(f, u'json') except Exception as e: raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e)) - self.mark_trusted_cells(nb, path, name) + self.mark_trusted_cells(nb, name, path) model['content'] = nb return model @@ -303,7 +303,7 @@ class FileNotebookManager(NotebookManager): os_path = self._get_os_path(new_name, new_path) nb = current.to_notebook_json(model['content']) - self.check_and_sign(nb, new_path, new_name) + self.check_and_sign(nb, new_name, new_path) if 'name' in nb['metadata']: nb['metadata']['name'] = u'' diff --git a/IPython/html/services/notebooks/handlers.py b/IPython/html/services/notebooks/handlers.py index de62bd9..dab6849 100644 --- a/IPython/html/services/notebooks/handlers.py +++ b/IPython/html/services/notebooks/handlers.py @@ -286,5 +286,3 @@ default_handlers = [ (r"/api/notebooks%s" % path_regex, NotebookHandler), ] - - diff --git a/IPython/html/services/notebooks/nbmanager.py b/IPython/html/services/notebooks/nbmanager.py index f3fb3fc..dae9d3e 100644 --- a/IPython/html/services/notebooks/nbmanager.py +++ b/IPython/html/services/notebooks/nbmanager.py @@ -52,7 +52,7 @@ class NotebookManager(LoggingConfigurable): Parameters ---------- path : string - The + The path to check Returns ------- @@ -224,22 +224,54 @@ class NotebookManager(LoggingConfigurable): def log_info(self): self.log.info(self.info_string()) - # NotebookManager methods provided for use in subclasses. - - def check_and_sign(self, nb, path, name): + def trust_notebook(self, name, path=''): + """Explicitly trust a notebook + + Parameters + ---------- + name : string + The filename of the notebook + path : string + The notebook's directory + """ + model = self.get_notebook(name, path) + nb = model['content'] + self.log.warn("Trusting notebook %s/%s", path, name) + self.notary.mark_cells(nb, True) + self.save_notebook(model, name, path) + + def check_and_sign(self, nb, name, path=''): """Check for trusted cells, and sign the notebook. Called as a part of saving notebooks. + + Parameters + ---------- + nb : dict + The notebook structure + name : string + The filename of the notebook + path : string + The notebook's directory """ if self.notary.check_cells(nb): self.notary.sign(nb) else: self.log.warn("Saving untrusted notebook %s/%s", path, name) - def mark_trusted_cells(self, nb, path, name): + def mark_trusted_cells(self, nb, name, path=''): """Mark cells as trusted if the notebook signature matches. Called as a part of loading notebooks. + + Parameters + ---------- + nb : dict + The notebook structure + name : string + The filename of the notebook + path : string + The notebook's directory """ trusted = self.notary.check_signature(nb) if not trusted: diff --git a/IPython/html/services/notebooks/tests/test_nbmanager.py b/IPython/html/services/notebooks/tests/test_nbmanager.py index fa37169..b4a55a3 100644 --- a/IPython/html/services/notebooks/tests/test_nbmanager.py +++ b/IPython/html/services/notebooks/tests/test_nbmanager.py @@ -2,12 +2,15 @@ """Tests for the notebook manager.""" from __future__ import print_function +import logging import os from tornado.web import HTTPError from unittest import TestCase from tempfile import NamedTemporaryFile +from IPython.nbformat import current + from IPython.utils.tempdir import TemporaryDirectory from IPython.utils.traitlets import TraitError from IPython.html.utils import url_path_join @@ -55,6 +58,17 @@ class TestFileNotebookManager(TestCase): class TestNotebookManager(TestCase): + def setUp(self): + self._temp_dir = TemporaryDirectory() + self.td = self._temp_dir.name + self.notebook_manager = FileNotebookManager( + notebook_dir=self.td, + log=logging.getLogger() + ) + + def tearDown(self): + self._temp_dir.cleanup() + def make_dir(self, abs_path, rel_path): """make subdirectory, rel_path is the relative path to that directory from the location where the server started""" @@ -63,183 +77,230 @@ class TestNotebookManager(TestCase): os.makedirs(os_path) except OSError: print("Directory already exists: %r" % os_path) - + + def add_code_cell(self, nb): + output = current.new_output("display_data", output_javascript="alert('hi');") + cell = current.new_code_cell("print('hi')", outputs=[output]) + if not nb.worksheets: + nb.worksheets.append(current.new_worksheet()) + nb.worksheets[0].cells.append(cell) + + def new_notebook(self): + nbm = self.notebook_manager + model = nbm.create_notebook() + name = model['name'] + path = model['path'] + + full_model = nbm.get_notebook(name, path) + nb = full_model['content'] + self.add_code_cell(nb) + + nbm.save_notebook(full_model, name, path) + return nb, name, path + def test_create_notebook(self): - with TemporaryDirectory() as td: - # Test in root directory - nm = FileNotebookManager(notebook_dir=td) - model = nm.create_notebook() - assert isinstance(model, dict) - self.assertIn('name', model) - self.assertIn('path', model) - self.assertEqual(model['name'], 'Untitled0.ipynb') - self.assertEqual(model['path'], '') - - # Test in sub-directory - sub_dir = '/foo/' - self.make_dir(nm.notebook_dir, 'foo') - model = nm.create_notebook(None, sub_dir) - assert isinstance(model, dict) - self.assertIn('name', model) - self.assertIn('path', model) - self.assertEqual(model['name'], 'Untitled0.ipynb') - self.assertEqual(model['path'], sub_dir.strip('/')) + nm = self.notebook_manager + # Test in root directory + model = nm.create_notebook() + assert isinstance(model, dict) + self.assertIn('name', model) + self.assertIn('path', model) + self.assertEqual(model['name'], 'Untitled0.ipynb') + self.assertEqual(model['path'], '') + + # Test in sub-directory + sub_dir = '/foo/' + self.make_dir(nm.notebook_dir, 'foo') + model = nm.create_notebook(None, sub_dir) + assert isinstance(model, dict) + self.assertIn('name', model) + self.assertIn('path', model) + self.assertEqual(model['name'], 'Untitled0.ipynb') + self.assertEqual(model['path'], sub_dir.strip('/')) def test_get_notebook(self): - with TemporaryDirectory() as td: - # Test in root directory - # Create a notebook - nm = FileNotebookManager(notebook_dir=td) - model = nm.create_notebook() - name = model['name'] - path = model['path'] - - # Check that we 'get' on the notebook we just created - model2 = nm.get_notebook(name, path) - assert isinstance(model2, dict) - self.assertIn('name', model2) - self.assertIn('path', model2) - self.assertEqual(model['name'], name) - self.assertEqual(model['path'], path) - - # Test in sub-directory - sub_dir = '/foo/' - self.make_dir(nm.notebook_dir, 'foo') - model = nm.create_notebook(None, sub_dir) - model2 = nm.get_notebook(name, sub_dir) - assert isinstance(model2, dict) - self.assertIn('name', model2) - self.assertIn('path', model2) - self.assertIn('content', model2) - self.assertEqual(model2['name'], 'Untitled0.ipynb') - self.assertEqual(model2['path'], sub_dir.strip('/')) + nm = self.notebook_manager + # Create a notebook + model = nm.create_notebook() + name = model['name'] + path = model['path'] + + # Check that we 'get' on the notebook we just created + model2 = nm.get_notebook(name, path) + assert isinstance(model2, dict) + self.assertIn('name', model2) + self.assertIn('path', model2) + self.assertEqual(model['name'], name) + self.assertEqual(model['path'], path) + + # Test in sub-directory + sub_dir = '/foo/' + self.make_dir(nm.notebook_dir, 'foo') + model = nm.create_notebook(None, sub_dir) + model2 = nm.get_notebook(name, sub_dir) + assert isinstance(model2, dict) + self.assertIn('name', model2) + self.assertIn('path', model2) + self.assertIn('content', model2) + self.assertEqual(model2['name'], 'Untitled0.ipynb') + self.assertEqual(model2['path'], sub_dir.strip('/')) def test_update_notebook(self): - with TemporaryDirectory() as td: - # Test in root directory - # Create a notebook - nm = FileNotebookManager(notebook_dir=td) - model = nm.create_notebook() - name = model['name'] - path = model['path'] - - # Change the name in the model for rename - model['name'] = 'test.ipynb' - model = nm.update_notebook(model, name, path) - assert isinstance(model, dict) - self.assertIn('name', model) - self.assertIn('path', model) - self.assertEqual(model['name'], 'test.ipynb') - - # Make sure the old name is gone - self.assertRaises(HTTPError, nm.get_notebook, name, path) - - # Test in sub-directory - # Create a directory and notebook in that directory - sub_dir = '/foo/' - self.make_dir(nm.notebook_dir, 'foo') - model = nm.create_notebook(None, sub_dir) - name = model['name'] - path = model['path'] - - # Change the name in the model for rename - model['name'] = 'test_in_sub.ipynb' - model = nm.update_notebook(model, name, path) - assert isinstance(model, dict) - self.assertIn('name', model) - self.assertIn('path', model) - self.assertEqual(model['name'], 'test_in_sub.ipynb') - self.assertEqual(model['path'], sub_dir.strip('/')) - - # Make sure the old name is gone - self.assertRaises(HTTPError, nm.get_notebook, name, path) + nm = self.notebook_manager + # Create a notebook + model = nm.create_notebook() + name = model['name'] + path = model['path'] + + # Change the name in the model for rename + model['name'] = 'test.ipynb' + model = nm.update_notebook(model, name, path) + assert isinstance(model, dict) + self.assertIn('name', model) + self.assertIn('path', model) + self.assertEqual(model['name'], 'test.ipynb') + + # Make sure the old name is gone + self.assertRaises(HTTPError, nm.get_notebook, name, path) + + # Test in sub-directory + # Create a directory and notebook in that directory + sub_dir = '/foo/' + self.make_dir(nm.notebook_dir, 'foo') + model = nm.create_notebook(None, sub_dir) + name = model['name'] + path = model['path'] + + # Change the name in the model for rename + model['name'] = 'test_in_sub.ipynb' + model = nm.update_notebook(model, name, path) + assert isinstance(model, dict) + self.assertIn('name', model) + self.assertIn('path', model) + self.assertEqual(model['name'], 'test_in_sub.ipynb') + self.assertEqual(model['path'], sub_dir.strip('/')) + + # Make sure the old name is gone + self.assertRaises(HTTPError, nm.get_notebook, name, path) def test_save_notebook(self): - with TemporaryDirectory() as td: - # Test in the root directory - # Create a notebook - nm = FileNotebookManager(notebook_dir=td) - model = nm.create_notebook() - name = model['name'] - path = model['path'] - - # Get the model with 'content' - full_model = nm.get_notebook(name, path) - - # Save the notebook - model = nm.save_notebook(full_model, name, path) - assert isinstance(model, dict) - self.assertIn('name', model) - self.assertIn('path', model) - self.assertEqual(model['name'], name) - self.assertEqual(model['path'], path) - - # Test in sub-directory - # Create a directory and notebook in that directory - sub_dir = '/foo/' - self.make_dir(nm.notebook_dir, 'foo') - model = nm.create_notebook(None, sub_dir) - name = model['name'] - path = model['path'] - model = nm.get_notebook(name, path) - - # Change the name in the model for rename - model = nm.save_notebook(model, name, path) - assert isinstance(model, dict) - self.assertIn('name', model) - self.assertIn('path', model) - self.assertEqual(model['name'], 'Untitled0.ipynb') - self.assertEqual(model['path'], sub_dir.strip('/')) + nm = self.notebook_manager + # Create a notebook + model = nm.create_notebook() + name = model['name'] + path = model['path'] + + # Get the model with 'content' + full_model = nm.get_notebook(name, path) + + # Save the notebook + model = nm.save_notebook(full_model, name, path) + assert isinstance(model, dict) + self.assertIn('name', model) + self.assertIn('path', model) + self.assertEqual(model['name'], name) + self.assertEqual(model['path'], path) + + # Test in sub-directory + # Create a directory and notebook in that directory + sub_dir = '/foo/' + self.make_dir(nm.notebook_dir, 'foo') + model = nm.create_notebook(None, sub_dir) + name = model['name'] + path = model['path'] + model = nm.get_notebook(name, path) + + # Change the name in the model for rename + model = nm.save_notebook(model, name, path) + assert isinstance(model, dict) + self.assertIn('name', model) + self.assertIn('path', model) + self.assertEqual(model['name'], 'Untitled0.ipynb') + self.assertEqual(model['path'], sub_dir.strip('/')) def test_save_notebook_with_script(self): - with TemporaryDirectory() as td: - # Create a notebook - nm = FileNotebookManager(notebook_dir=td) - nm.save_script = True - model = nm.create_notebook() - name = model['name'] - path = model['path'] + nm = self.notebook_manager + # Create a notebook + model = nm.create_notebook() + nm.save_script = True + model = nm.create_notebook() + name = model['name'] + path = model['path'] - # Get the model with 'content' - full_model = nm.get_notebook(name, path) + # Get the model with 'content' + full_model = nm.get_notebook(name, path) - # Save the notebook - model = nm.save_notebook(full_model, name, path) + # Save the notebook + model = nm.save_notebook(full_model, name, path) - # Check that the script was created - py_path = os.path.join(td, os.path.splitext(name)[0]+'.py') - assert os.path.exists(py_path), py_path + # Check that the script was created + py_path = os.path.join(nm.notebook_dir, os.path.splitext(name)[0]+'.py') + assert os.path.exists(py_path), py_path def test_delete_notebook(self): - with TemporaryDirectory() as td: - # Test in the root directory - # Create a notebook - nm = FileNotebookManager(notebook_dir=td) - model = nm.create_notebook() - name = model['name'] - path = model['path'] - - # Delete the notebook - nm.delete_notebook(name, path) - - # Check that a 'get' on the deleted notebook raises and error - self.assertRaises(HTTPError, nm.get_notebook, name, path) + nm = self.notebook_manager + # Create a notebook + nb, name, path = self.new_notebook() + + # Delete the notebook + nm.delete_notebook(name, path) + + # Check that a 'get' on the deleted notebook raises and error + self.assertRaises(HTTPError, nm.get_notebook, name, path) def test_copy_notebook(self): - with TemporaryDirectory() as td: - # Test in the root directory - # Create a notebook - nm = FileNotebookManager(notebook_dir=td) - path = u'å b' - name = u'nb √.ipynb' - os.mkdir(os.path.join(td, path)) - orig = nm.create_notebook({'name' : name}, path=path) - - # copy with unspecified name - copy = nm.copy_notebook(name, path=path) - self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb')) - - # copy with specified name - copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path) - self.assertEqual(copy2['name'], u'copy 2.ipynb') + nm = self.notebook_manager + path = u'å b' + name = u'nb √.ipynb' + os.mkdir(os.path.join(nm.notebook_dir, path)) + orig = nm.create_notebook({'name' : name}, path=path) + + # copy with unspecified name + copy = nm.copy_notebook(name, path=path) + self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb')) + + # copy with specified name + copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path) + self.assertEqual(copy2['name'], u'copy 2.ipynb') + def test_trust_notebook(self): + nbm = self.notebook_manager + nb, name, path = self.new_notebook() + + untrusted = nbm.get_notebook(name, path)['content'] + assert not nbm.notary.check_cells(untrusted) + + # print(untrusted) + nbm.trust_notebook(name, path) + trusted = nbm.get_notebook(name, path)['content'] + # print(trusted) + assert nbm.notary.check_cells(trusted) + + def test_mark_trusted_cells(self): + nbm = self.notebook_manager + nb, name, path = self.new_notebook() + + nbm.mark_trusted_cells(nb, name, path) + for cell in nb.worksheets[0].cells: + if cell.cell_type == 'code': + assert not cell.trusted + + nbm.trust_notebook(name, path) + nb = nbm.get_notebook(name, path)['content'] + for cell in nb.worksheets[0].cells: + if cell.cell_type == 'code': + assert cell.trusted + + def test_check_and_sign(self): + nbm = self.notebook_manager + nb, name, path = self.new_notebook() + + nbm.mark_trusted_cells(nb, name, path) + nbm.check_and_sign(nb, name, path) + assert not nbm.notary.check_signature(nb) + + nbm.trust_notebook(name, path) + nb = nbm.get_notebook(name, path)['content'] + nbm.mark_trusted_cells(nb, name, path) + nbm.check_and_sign(nb, name, path) + assert nbm.notary.check_signature(nb) diff --git a/IPython/html/static/base/js/security.js b/IPython/html/static/base/js/security.js new file mode 100644 index 0000000..ecdcb5b --- /dev/null +++ b/IPython/html/static/base/js/security.js @@ -0,0 +1,126 @@ +//---------------------------------------------------------------------------- +// Copyright (C) 2014 The IPython Development Team +// +// Distributed under the terms of the BSD License. The full license is in +// the file COPYING, distributed as part of this software. +//---------------------------------------------------------------------------- + +//============================================================================ +// Utilities +//============================================================================ +IPython.namespace('IPython.security'); + +IPython.security = (function (IPython) { + "use strict"; + + var utils = IPython.utils; + + var noop = function (x) { return x; }; + + var caja; + if (window && window.html) { + caja = window.html; + caja.html4 = window.html4; + caja.sanitizeStylesheet = window.sanitizeStylesheet; + } + + var sanitizeAttribs = function (tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) { + // add trusting data-attributes to the default sanitizeAttribs from caja + // this function is mostly copied from the caja source + var ATTRIBS = caja.html4.ATTRIBS; + for (var i = 0; i < attribs.length; i += 2) { + var attribName = attribs[i]; + if (attribName.substr(0,5) == 'data-') { + var attribKey = '*::' + attribName; + if (!ATTRIBS.hasOwnProperty(attribKey)) { + ATTRIBS[attribKey] = 0; + } + } + } + return caja.sanitizeAttribs(tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger); + }; + + var sanitize_css = function (css, tagPolicy) { + // sanitize CSS + // like sanitize_html, but for CSS + // called by sanitize_stylesheets + return caja.sanitizeStylesheet( + window.location.pathname, + css, + { + containerClass: null, + idSuffix: '', + tagPolicy: tagPolicy, + virtualizeAttrName: noop + }, + noop + ); + }; + + var sanitize_stylesheets = function (html, tagPolicy) { + // sanitize just the css in style tags in a block of html + // called by sanitize_html, if allow_css is true + var h = $("
").append(html); + var style_tags = h.find("style"); + if (!style_tags.length) { + // no style tags to sanitize + return html; + } + style_tags.each(function(i, style) { + style.innerHTML = sanitize_css(style.innerHTML, tagPolicy); + }); + return h.html(); + }; + + var sanitize_html = function (html, allow_css) { + // sanitize HTML + // if allow_css is true (default: false), CSS is sanitized as well. + // otherwise, CSS elements and attributes are simply removed. + var html4 = caja.html4; + + if (allow_css) { + // allow sanitization of style tags, + // not just scrubbing + html4.ELEMENTS.style &= ~html4.eflags.UNSAFE; + html4.ATTRIBS.style = html4.atype.STYLE; + } else { + // scrub all CSS + html4.ELEMENTS.style |= html4.eflags.UNSAFE; + html4.ATTRIBS.style = html4.atype.SCRIPT; + } + + var record_messages = function (msg, opts) { + console.log("HTML Sanitizer", msg, opts); + }; + + var policy = function (tagName, attribs) { + if (!(html4.ELEMENTS[tagName] & html4.eflags.UNSAFE)) { + return { + 'attribs': sanitizeAttribs(tagName, attribs, + noop, noop, record_messages) + }; + } else { + record_messages(tagName + " removed", { + change: "removed", + tagName: tagName + }); + } + }; + + var sanitized = caja.sanitizeWithPolicy(html, policy); + + if (allow_css) { + // sanitize style tags as stylesheets + sanitized = sanitize_stylesheets(result.sanitized, policy); + } + + return sanitized; + }; + + return { + caja: caja, + sanitize_html: sanitize_html + }; + +}(IPython)); + diff --git a/IPython/html/static/base/js/utils.js b/IPython/html/static/base/js/utils.js index cfd138b..c840924 100644 --- a/IPython/html/static/base/js/utils.js +++ b/IPython/html/static/base/js/utils.js @@ -488,7 +488,6 @@ IPython.utils = (function (IPython) { } } - return { regex_split : regex_split, uuid : uuid, diff --git a/IPython/html/static/components b/IPython/html/static/components index 2f89587..7a9ba81 160000 --- a/IPython/html/static/components +++ b/IPython/html/static/components @@ -1 +1 @@ -Subproject commit 2f8958788c7e0416e5c44f532e9630a658df11fd +Subproject commit 7a9ba818b3e13123621cb5ff336c002d49470f55 diff --git a/IPython/html/static/notebook/js/menubar.js b/IPython/html/static/notebook/js/menubar.js index dacf18e..8eeb4b7 100644 --- a/IPython/html/static/notebook/js/menubar.js +++ b/IPython/html/static/notebook/js/menubar.js @@ -133,6 +133,20 @@ var IPython = (function (IPython) { }); this.element.find('#restore_checkpoint').click(function () { }); + this.element.find('#trust_notebook').click(function () { + IPython.notebook.trust_notebook(); + }); + $([IPython.events]).on('trust_changed.Notebook', function (event, trusted) { + if (trusted) { + that.element.find('#trust_notebook') + .addClass("disabled") + .find("a").text("Trusted Notebook"); + } else { + that.element.find('#trust_notebook') + .removeClass("disabled") + .find("a").text("Trust Notebook"); + } + }); this.element.find('#kill_and_exit').click(function () { IPython.notebook.session.delete(); setTimeout(function(){ diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index d49fd49..85f250c 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -110,6 +110,10 @@ var IPython = (function (IPython) { that.dirty = data.value; }); + $([IPython.events]).on('trust_changed.Notebook', function (event, data) { + that.trusted = data.value; + }); + $([IPython.events]).on('select.Cell', function (event, data) { var index = that.find_cell_index(data.cell); that.select(index); @@ -1607,6 +1611,7 @@ var IPython = (function (IPython) { // Save the metadata and name. this.metadata = content.metadata; this.notebook_name = data.name; + var trusted = true; // Only handle 1 worksheet for now. var worksheet = content.worksheets[0]; if (worksheet !== undefined) { @@ -1627,8 +1632,15 @@ var IPython = (function (IPython) { new_cell = this.insert_cell_at_index(cell_data.cell_type, i); new_cell.fromJSON(cell_data); + if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) { + trusted = false; + } } } + if (trusted != this.trusted) { + this.trusted = trusted; + $([IPython.events]).trigger("trust_changed.Notebook", trusted); + } if (content.worksheets.length > 1) { IPython.dialog.modal({ title : "Multiple worksheets", @@ -1654,8 +1666,13 @@ var IPython = (function (IPython) { var cells = this.get_cells(); var ncells = cells.length; var cell_array = new Array(ncells); + var trusted = true; for (var i=0; i").append($("

") + .text("A trusted IPython notebook may execute hidden malicious code ") + .append($("") + .append( + $("").text("when you open it") + ) + ).append(".").append( + " Selecting trust will immediately reload this notebook in a trusted state." + ).append( + " For more information, see the " + ).append($("").attr("href", "http://ipython.org/security.html") + .text("IPython security documentation") + ).append(".") + ); + + var nb = this; + IPython.dialog.modal({ + title: "Trust this notebook?", + body: body, + + buttons: { + Cancel : {}, + Trust : { + class : "btn-danger", + click : function () { + var cells = nb.get_cells(); + for (var i = 0; i < cells.length; i++) { + var cell = cells[i]; + if (cell.cell_type == 'code') { + cell.output_area.trusted = true; + } + } + $([IPython.events]).on('notebook_saved.Notebook', function () { + window.location.reload(); + }); + nb.save_notebook(); + } + } + } + }); + }; + Notebook.prototype.new_notebook = function(){ var path = this.notebook_path; var base_url = this.base_url; diff --git a/IPython/html/static/notebook/js/outputarea.js b/IPython/html/static/notebook/js/outputarea.js index d4e1456..12d39da 100644 --- a/IPython/html/static/notebook/js/outputarea.js +++ b/IPython/html/static/notebook/js/outputarea.js @@ -480,6 +480,7 @@ var IPython = (function (IPython) { OutputArea.safe_outputs = { 'text/plain' : true, + 'text/latex' : true, 'image/png' : true, 'image/jpeg' : true }; @@ -489,18 +490,20 @@ var IPython = (function (IPython) { var type = OutputArea.display_order[type_i]; var append = OutputArea.append_map[type]; if ((json[type] !== undefined) && append) { + var value = json[type]; if (!this.trusted && !OutputArea.safe_outputs[type]) { - // not trusted show warning and do not display - var content = { - text : "Untrusted " + type + " output ignored.", - stream : "stderr" + // not trusted, sanitize HTML + if (type==='text/html' || type==='text/svg') { + value = IPython.security.sanitize_html(value); + } else { + // don't display if we don't know how to sanitize it + console.log("Ignoring untrusted " + type + " output."); + continue; } - this.append_stream(content); - continue; } var md = json.metadata || {}; - var toinsert = append.apply(this, [json[type], md, element]); - $([IPython.events]).trigger('output_appended.OutputArea', [type, json[type], md, toinsert]); + var toinsert = append.apply(this, [value, md, element]); + $([IPython.events]).trigger('output_appended.OutputArea', [type, value, md, toinsert]); return true; } } diff --git a/IPython/html/static/notebook/js/textcell.js b/IPython/html/static/notebook/js/textcell.js index 65d8e3a..70b6346 100644 --- a/IPython/html/static/notebook/js/textcell.js +++ b/IPython/html/static/notebook/js/textcell.js @@ -21,6 +21,7 @@ var IPython = (function (IPython) { // TextCell base class var keycodes = IPython.keyboard.keycodes; + var security = IPython.security; /** * Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text' @@ -344,23 +345,12 @@ var IPython = (function (IPython) { text = text_and_math[0]; math = text_and_math[1]; var html = marked.parser(marked.lexer(text)); - html = $(IPython.mathjaxutils.replace_math(html, math)); - // Links in markdown cells should open in new tabs. + html = IPython.mathjaxutils.replace_math(html, math); + html = security.sanitize_html(html); + html = $(html); + // links in markdown cells should open in new tabs html.find("a[href]").not('[href^="#"]').attr("target", "_blank"); - try { - // TODO: This HTML needs to be treated as potentially dangerous - // user input and should be handled before set_rendered. - this.set_rendered(html); - } catch (e) { - console.log("Error running Javascript in Markdown:"); - console.log(e); - this.set_rendered( - $("

") - .append($("
").text('Error rendering Markdown!').addClass("js-error")) - .append($("
").text(e.toString()).addClass("js-error")) - .html() - ); - } + this.set_rendered(html); this.element.find('div.input_area').hide(); this.element.find("div.text_cell_render").show(); this.typeset(); @@ -528,7 +518,9 @@ var IPython = (function (IPython) { text = text_and_math[0]; math = text_and_math[1]; var html = marked.parser(marked.lexer(text)); - var h = $(IPython.mathjaxutils.replace_math(html, math)); + html = IPython.mathjaxutils.replace_math(html, math); + html = security.sanitize_html(html); + var h = $(html); // add id and linkback anchor var hash = h.text().replace(/ /g, '-'); h.attr('id', hash); @@ -538,13 +530,10 @@ var IPython = (function (IPython) { .attr('href', '#' + hash) .text('¶') ); - // TODO: This HTML needs to be treated as potentially dangerous - // user input and should be handled before set_rendered. this.set_rendered(h); - this.typeset(); - this.element.find('div.input_area').hide(); + this.element.find('div.text_cell_input').hide(); this.element.find("div.text_cell_render").show(); - + this.typeset(); } return cont; }; diff --git a/IPython/html/static/notebook/less/style_noapp.less b/IPython/html/static/notebook/less/style_noapp.less index d38481c..fad009b 100644 --- a/IPython/html/static/notebook/less/style_noapp.less +++ b/IPython/html/static/notebook/less/style_noapp.less @@ -7,4 +7,4 @@ @import "outputarea.less"; @import "renderedhtml.less"; @import "textcell.less"; -@import "widgets.less"; +@import "../../widgets/less/widgets.less"; diff --git a/IPython/html/templates/notebook.html b/IPython/html/templates/notebook.html index 104d4e3..c1adc21 100644 --- a/IPython/html/templates/notebook.html +++ b/IPython/html/templates/notebook.html @@ -86,7 +86,10 @@ class="notebook_app"
  • - +
  • + Trust Notebook
  • +
  • Close and halt
  • @@ -291,6 +294,7 @@ class="notebook_app" {{super()}} + + diff --git a/IPython/html/tests/base/security.js b/IPython/html/tests/base/security.js new file mode 100644 index 0000000..af23e66 --- /dev/null +++ b/IPython/html/tests/base/security.js @@ -0,0 +1,57 @@ +safe_tests = [ + "

    Hi there

    ", + '

    Hi There!

    ', + 'citation', + '
    Hi There
    ', +]; + +unsafe_tests = [ + "", + '999', + '999', + '">', + '', + '<', + '