Merge Security Pull Request: google-caja

Adds HTML sanitization.

The basics:

- untrusted HTML is always sanitized, with no warning
  (there is console logging for changes made)
- markdown is always treated as untrusted
- no warnings for simply excluded output (e.g. Javascript)
- CSS tags and attributes are always stripped from untrusted HTML
- never check whether HTML is "safe," only sanitize
- add 'Trust notebook' to File menu
MinRK 12 years ago
commit 3588fe40a9

@ -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''

@ -286,5 +286,3 @@ default_handlers = [
(r"/api/notebooks%s" % path_regex, NotebookHandler),
]

@ -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:

@ -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)

@ -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 = $("<div/>").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));

@ -488,7 +488,6 @@ IPython.utils = (function (IPython) {
}
}
return {
regex_split : regex_split,
uuid : uuid,

@ -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(){

@ -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<ncells; i++) {
cell_array[i] = cells[i].toJSON();
var cell = cells[i];
if (cell.cell_type == 'code' && !cell.output_area.trusted) {
trusted = false;
}
cell_array[i] = cell.toJSON();
}
var data = {
// Only handle 1 worksheet for now.
@ -1665,6 +1682,10 @@ var IPython = (function (IPython) {
}],
metadata : this.metadata
};
if (trusted != this.trusted) {
this.trusted = trusted;
$([IPython.events]).trigger("trust_changed.Notebook", trusted);
}
return data;
};
@ -1786,6 +1807,54 @@ var IPython = (function (IPython) {
$([IPython.events]).trigger('notebook_save_failed.Notebook', [xhr, status, error]);
};
/**
* Explicitly trust the output of this notebook.
*
* @method trust_notebook
*/
Notebook.prototype.trust_notebook = function (extra_settings) {
var body = $("<div>").append($("<p>")
.text("A trusted IPython notebook may execute hidden malicious code ")
.append($("<strong>")
.append(
$("<em>").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($("<a>").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;

@ -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;
}
}

@ -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(
$("<div/>")
.append($("<div/>").text('Error rendering Markdown!').addClass("js-error"))
.append($("<div/>").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;
};

@ -7,4 +7,4 @@
@import "outputarea.less";
@import "renderedhtml.less";
@import "textcell.less";
@import "widgets.less";
@import "../../widgets/less/widgets.less";

@ -86,7 +86,10 @@ class="notebook_app"
</ul>
</li>
<li class="divider"></li>
<li id="trust_notebook"
title="Trust the output of this notebook">
<a href="#" >Trust Notebook</a></li>
<li class="divider"></li>
<li id="kill_and_exit"
title="Shutdown this notebook's kernel, and close this window">
<a href="#" >Close and halt</a></li>
@ -291,6 +294,7 @@ class="notebook_app"
{{super()}}
<script src="{{ static_url("components/google-caja/html-css-sanitizer-minified.js") }}" charset="utf-8"></script>
<script src="{{ static_url("components/codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
<script type="text/javascript">
CodeMirror.modeURL = "{{ static_url("components/codemirror/mode/%N/%N.js", include_version=False) }}";
@ -318,6 +322,7 @@ class="notebook_app"
<script src="{{ static_url("base/js/events.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ static_url("base/js/keyboard.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ static_url("base/js/security.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ static_url("services/kernels/js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ static_url("services/kernels/js/comm.js") }}" type="text/javascript" charset="utf-8"></script>

@ -0,0 +1,57 @@
safe_tests = [
"<p>Hi there</p>",
'<h1 class="foo">Hi There!</h1>',
'<a data-cite="foo">citation</a>',
'<div><span>Hi There</span></div>',
];
unsafe_tests = [
"<script>alert(999);</script>",
'<a onmouseover="alert(999)">999</a>',
'<a onmouseover=alert(999)>999</a>',
'<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
'<IMG SRC=# onmouseover="alert(999)">',
'<<SCRIPT>alert(999);//<</SCRIPT>',
'<SCRIPT SRC=http://ha.ckers.org/xss.js?< B >',
'<META HTTP-EQUIV="refresh" CONTENT="0;url=data:text/html base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">',
'<META HTTP-EQUIV="refresh" CONTENT="0; URL=http://;URL=javascript:alert(999);">',
'<IFRAME SRC="javascript:alert(999);"></IFRAME>',
'<IFRAME SRC=# onmouseover="alert(document.cookie)"></IFRAME>',
'<EMBED SRC="data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dH A6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv MjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hs aW5rIiB2ZXJzaW9uPSIxLjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxOTQiIGhlaWdodD0iMjAw IiBpZD0ieHNzIj48c2NyaXB0IHR5cGU9InRleHQvZWNtYXNjcmlwdCI+YWxlcnQoIlh TUyIpOzwvc2NyaXB0Pjwvc3ZnPg==" type="image/svg+xml" AllowScriptAccess="always"></EMBED>',
// CSS is scrubbed
'<style src="http://untrusted/style.css"></style>',
'<style>div#notebook { background-color: alert-red; }</style>',
'<div style="background-color: alert-red;"></div>',
];
var truncate = function (s, n) {
// truncate a string with an ellipsis
if (s.length > n) {
return s.substr(0, n-3) + '...';
} else {
return s;
}
};
casper.notebook_test(function () {
this.each(safe_tests, function (self, item) {
var sanitized = self.evaluate(function (item) {
return IPython.security.sanitize_html(item);
}, item);
// string equality may be too strict, but it works for now
this.test.assertEquals(sanitized, item, "Safe: '" + truncate(item, 32) + "'");
});
this.each(unsafe_tests, function (self, item) {
var sanitized = self.evaluate(function (item) {
return IPython.security.sanitize_html(item);
}, item);
this.test.assertNotEquals(sanitized, item,
"Sanitized: '" + truncate(item, 32) +
"' => '" + truncate(sanitized, 32) + "'"
);
this.test.assertEquals(sanitized.indexOf("alert"), -1, "alert removed");
});
});

@ -150,6 +150,7 @@ def find_package_data():
pjoin(components, "backbone", "backbone-min.js"),
pjoin(components, "bootstrap", "bootstrap", "js", "bootstrap.min.js"),
pjoin(components, "font-awesome", "font", "*.*"),
pjoin(components, "google-caja", "html-css-sanitizer-minified.js"),
pjoin(components, "highlight.js", "build", "highlight.pack.js"),
pjoin(components, "jquery", "jquery.min.js"),
pjoin(components, "jquery-ui", "ui", "minified", "jquery-ui.min.js"),

Loading…
Cancel
Save