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
commit
3588fe40a9
@ -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));
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in new issue