whitelist google.com/maps iframes

This commit is contained in:
Régis Hanol 2013-11-29 18:08:53 +01:00
parent 8c8645f158
commit 9b6538832d
4 changed files with 70 additions and 35 deletions

View File

@ -409,7 +409,7 @@ URI.prototype.setPath = function (newPath) {
URI.prototype.setRawPath = function (newPath) { URI.prototype.setRawPath = function (newPath) {
if (newPath) { if (newPath) {
newPath = String(newPath); newPath = String(newPath);
this.path_ = this.path_ =
// Paths must start with '/' unless this is a path-relative URL. // Paths must start with '/' unless this is a path-relative URL.
(!this.domain_ || /^\//.test(newPath)) ? newPath : '/' + newPath; (!this.domain_ || /^\//.test(newPath)) ? newPath : '/' + newPath;
} else { } else {
@ -898,6 +898,7 @@ html4.ATTRIBS = {
'iframe::marginheight': 0, 'iframe::marginheight': 0,
'iframe::marginwidth': 0, 'iframe::marginwidth': 0,
'iframe::width': 0, 'iframe::width': 0,
'iframe::src': 1,
'img::align': 0, 'img::align': 0,
'img::alt': 0, 'img::alt': 0,
'img::border': 0, 'img::border': 0,
@ -1293,6 +1294,7 @@ html4.URIEFFECTS = {
'command::icon': 1, 'command::icon': 1,
'del::cite': 0, 'del::cite': 0,
'form::action': 2, 'form::action': 2,
'iframe::src': 1,
'img::src': 1, 'img::src': 1,
'input::src': 1, 'input::src': 1,
'ins::cite': 0, 'ins::cite': 0,
@ -1315,6 +1317,7 @@ html4.LOADERTYPES = {
'command::icon': 1, 'command::icon': 1,
'del::cite': 2, 'del::cite': 2,
'form::action': 2, 'form::action': 2,
'iframe::src': 2,
'img::src': 1, 'img::src': 1,
'input::src': 1, 'input::src': 1,
'ins::cite': 2, 'ins::cite': 2,
@ -1323,6 +1326,15 @@ html4.LOADERTYPES = {
'video::src': 2 'video::src': 2
}; };
html4[ 'LOADERTYPES' ] = html4.LOADERTYPES; html4[ 'LOADERTYPES' ] = html4.LOADERTYPES;
// NOTE: currently focused only on URI-type attributes
html4.REQUIREDATTRIBUTES = {
"audio" : ["src"],
"form" : ["action"],
"iframe" : ["src"],
"image" : ["src"],
"video" : ["src"]
};
html4[ 'REQUIREDATTRIBUTES' ] = html4.REQUIREDATTRIBUTES;
// export for Closure Compiler // export for Closure Compiler
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window['html4'] = html4; window['html4'] = html4;
@ -2194,8 +2206,7 @@ var html = (function(html4) {
* @return {Array.<?string>} The sanitized attributes as a list of alternating * @return {Array.<?string>} The sanitized attributes as a list of alternating
* names and values, where a null value means to omit the attribute. * names and values, where a null value means to omit the attribute.
*/ */
function sanitizeAttribs(tagName, attribs, function sanitizeAttribs(tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) {
opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) {
// TODO(felix8a): it's obnoxious that domado duplicates much of this // TODO(felix8a): it's obnoxious that domado duplicates much of this
// TODO(felix8a): maybe consistently enforce constraints like target= // TODO(felix8a): maybe consistently enforce constraints like target=
for (var i = 0; i < attribs.length; i += 2) { for (var i = 0; i < attribs.length; i += 2) {
@ -2277,7 +2288,7 @@ var html = (function(html4) {
"XML_ATTR": attribName, "XML_ATTR": attribName,
"XML_TAG": tagName "XML_TAG": tagName
}, opt_naiveUriRewriter); }, opt_naiveUriRewriter);
if (opt_logger) { if (opt_logger) {
log(opt_logger, tagName, attribName, oldValue, value); log(opt_logger, tagName, attribName, oldValue, value);
} }
break; break;
@ -2325,14 +2336,13 @@ var html = (function(html4) {
* @return {function(string, Array.<?string>)} A tagPolicy suitable for * @return {function(string, Array.<?string>)} A tagPolicy suitable for
* passing to html.sanitize. * passing to html.sanitize.
*/ */
function makeTagPolicy( function makeTagPolicy(opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) {
opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) {
return function(tagName, attribs) { return function(tagName, attribs) {
if (!(html4.ELEMENTS[tagName] & html4.eflags['UNSAFE'])) { if (!(html4.ELEMENTS[tagName] & html4.eflags['UNSAFE'])) {
return { var sanitizedAttribs = sanitizeAttribs(tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger);
'attribs': sanitizeAttribs(tagName, attribs, var requiredAttributes = html4.REQUIREDATTRIBUTES[tagName];
opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) if (requiredAttributes && missRequiredAttributes(sanitizedAttribs, requiredAttributes)) { return }
}; return { 'attribs': sanitizedAttribs };
} else { } else {
if (opt_logger) { if (opt_logger) {
log(opt_logger, tagName, undefined, undefined, undefined); log(opt_logger, tagName, undefined, undefined, undefined);
@ -2341,6 +2351,16 @@ var html = (function(html4) {
}; };
} }
function missRequiredAttributes(sanitizedAttributes, requiredAttributes) {
var requiredAttributesWithValueCount = 0;
for (var i = 0, length = sanitizedAttributes.length; i < length; i += 2) {
var name = sanitizedAttributes[i];
var value = sanitizedAttributes[i + 1];
if (requiredAttributes.indexOf(name) > -1 && value && value.length > 0) { requiredAttributesWithValueCount++; }
}
return requiredAttributesWithValueCount != requiredAttributes.length;
}
/** /**
* Sanitizes HTML tags and attributes according to a given policy. * Sanitizes HTML tags and attributes according to a given policy.
* @param {string} inputHtml The HTML to sanitize. * @param {string} inputHtml The HTML to sanitize.
@ -2364,10 +2384,8 @@ var html = (function(html4) {
* to attributes containing HTML names, element IDs, and space-separated * to attributes containing HTML names, element IDs, and space-separated
* lists of classes. If not given, such attributes are left unchanged. * lists of classes. If not given, such attributes are left unchanged.
*/ */
function sanitize(inputHtml, function sanitize(inputHtml, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) {
opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) { var tagPolicy = makeTagPolicy(opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger);
var tagPolicy = makeTagPolicy(
opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger);
return sanitizeWithPolicy(inputHtml, tagPolicy); return sanitizeWithPolicy(inputHtml, tagPolicy);
} }

View File

@ -3,7 +3,7 @@
**/ **/
var blockTags = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'div', var blockTags = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'div',
'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'noscript', 'ol', 'output', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'iframe', 'noscript', 'ol', 'output',
'p', 'pre', 'section', 'table', 'tfoot', 'ul', 'video'], 'p', 'pre', 'section', 'table', 'tfoot', 'ul', 'video'],
splitAtLast = function(tag, block, next, first) { splitAtLast = function(tag, block, next, first) {
@ -39,4 +39,4 @@ Discourse.Dialect.registerBlock('html', function(block, next) {
return [ block.toString() ]; return [ block.toString() ];
} }
} }
}); });

View File

@ -10,6 +10,7 @@
Discourse.Markdown = { Discourse.Markdown = {
validClasses: {}, validClasses: {},
validIframes: [],
/** /**
Whitelists classes for sanitization Whitelists classes for sanitization
@ -21,9 +22,17 @@ Discourse.Markdown = {
var args = Array.prototype.slice.call(arguments), var args = Array.prototype.slice.call(arguments),
validClasses = Discourse.Markdown.validClasses; validClasses = Discourse.Markdown.validClasses;
args.forEach(function (a) { args.forEach(function (a) { validClasses[a] = true; });
validClasses[a] = true; },
});
/**
Whitelists iframes for sanitization
@method whiteListIframe
@param {Regexp} regexp The regexp to whitelist.
**/
whiteListIframe: function(regexp) {
Discourse.Markdown.validIframes.push(regexp);
}, },
/** /**
@ -38,8 +47,7 @@ Discourse.Markdown = {
if (!opts) opts = {}; if (!opts) opts = {};
// Make sure we've got a string // Make sure we've got a string
if (!raw) return ""; if (!raw || raw.length === 0) return "";
if (raw.length === 0) return "";
return this.markdownConverter(opts).makeHtml(raw); return this.markdownConverter(opts).makeHtml(raw);
}, },
@ -52,7 +60,6 @@ Discourse.Markdown = {
@return {Markdown.Editor} the editor instance @return {Markdown.Editor} the editor instance
**/ **/
createEditor: function(converterOptions) { createEditor: function(converterOptions) {
if (!converterOptions) converterOptions = {}; if (!converterOptions) converterOptions = {};
// By default we always sanitize content in the editor // By default we always sanitize content in the editor
@ -109,9 +116,21 @@ Discourse.Markdown = {
@param {String} url Url to check @param {String} url Url to check
@return {String} url to insert in the cooked content @return {String} url to insert in the cooked content
**/ **/
urlAllowed: function (url) { urlAllowed: function (uri, effect, ltype, hints) {
if(/^https?:\/\//.test(url)) { return url; } var url = typeof(uri) === "string" ? uri : uri.toString();
if(/^\/\/?[\w\.\-]+/.test(url)) { return url; }
// whitelist some iframe only
if (hints && hints.XML_TAG === "iframe" && hints.XML_ATTR === "src") {
for (var i = 0, length = Discourse.Markdown.validIframes.length; i < length; i++) {
if(Discourse.Markdown.validIframes[i].test(url)) { return url; }
}
return;
}
// absolute urls
if(/^(https?:)?\/\/[\w\.\-]+/i.test(url)) { return url; }
// relative urls
if(/^\/[\w\.\-]+/i.test(url)) { return url; }
}, },
/** /**
@ -149,11 +168,8 @@ Discourse.Markdown = {
return { return {
makeHtml: function(text) { makeHtml: function(text) {
text = Discourse.Dialect.cook(text, opts); text = Discourse.Dialect.cook(text, opts);
if (!text) return ""; return !text ? "" : text;
return text;
} }
}; };
} }
@ -162,3 +178,4 @@ Discourse.Markdown = {
RSVP.EventTarget.mixin(Discourse.Markdown); RSVP.EventTarget.mixin(Discourse.Markdown);
Discourse.Markdown.whiteListClass("attachment"); Discourse.Markdown.whiteListClass("attachment");
Discourse.Markdown.whiteListIframe(/^(https?:)?\/\/www\.google\.com\/maps\/embed\?.+/i);

View File

@ -8,12 +8,6 @@ module("Discourse.Markdown", {
var cooked = function(input, expected, text) { var cooked = function(input, expected, text) {
var result = Discourse.Markdown.cook(input, {mentionLookup: false, sanitize: true}); var result = Discourse.Markdown.cook(input, {mentionLookup: false, sanitize: true});
if (result !== expected) {
console.log(JSON.stringify(result));
console.log(JSON.stringify(expected));
}
equal(result, expected, text); equal(result, expected, text);
}; };
@ -337,6 +331,12 @@ test("sanitize", function() {
cooked("<table><tr><td>hello</td></tr></table>\nafter", "<p>after</p>", "it does not allow tables"); cooked("<table><tr><td>hello</td></tr></table>\nafter", "<p>after</p>", "it does not allow tables");
cooked("<blockquote>a\n</blockquote>\n", "<blockquote>a\n\n<br/>\n\n</blockquote>", "it does not double sanitize"); cooked("<blockquote>a\n</blockquote>\n", "<blockquote>a\n\n<br/>\n\n</blockquote>", "it does not double sanitize");
cooked("<iframe src=\"http://discourse.org\" width=\"100\" height=\"42\"></iframe>", "", "it does not allow most iframe");
cooked("<iframe src=\"https://www.google.com/maps/embed?pb=!1m10!1m8!1m3!1d2624.9983685732213!2d2.29432085!3d48.85824149999999!3m2!1i1024!2i768!4f13.1!5e0!3m2!1sen!2s!4v1385737436368\" width=\"100\" height=\"42\"></iframe>",
"<iframe src=\"https://www.google.com/maps/embed?pb=!1m10!1m8!1m3!1d2624.9983685732213!2d2.29432085!3d48.85824149999999!3m2!1i1024!2i768!4f13.1!5e0!3m2!1sen!2s!4v1385737436368\" width=\"100\" height=\"42\"></iframe>",
"it allows iframe to google maps");
}); });
test("URLs in BBCode tags", function() { test("URLs in BBCode tags", function() {