diff --git a/README.md b/README.md index 74221419..307c370a 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ - 支持 AMD / CMD 模块化加载(支持 [Require.js](https://pandao.github.io/editor.md/examples/use-requirejs.html) & [Sea.js](https://pandao.github.io/editor.md/examples/use-seajs.html)),并且支持[自定义扩展插件](https://pandao.github.io/editor.md/examples/define-plugin.html); - 兼容主流的浏览器(IE8+)和 [Zepto.js](https://pandao.github.io/editor.md/examples/use-zepto.html),且支持 iPad 等平板设备; -#### Download & install - +#### Download & install + Download: [Github download](https://github.com/pandao/editor.md/archive/master.zip) @@ -58,13 +58,13 @@ Bower install : bower install editor.md ``` -#### Usages - +#### Usages + ##### Create a Markdown editor ```html -
+
@@ -72,9 +72,9 @@ bower install editor.md -``` - -> See the full example: [http://editor.md.ipandao.com/examples/html-preview-markdown-to-html.html](http://editor.md.ipandao.com/examples/html-preview-markdown-to-html.html) - -##### HTML to Markdown? - + // htmlDecode : "style,script,iframe|on*", // Note: If enabled, you should filter some dangerous HTML tags for website security, you can also filter trigers. + }); + }); + +``` + +> See the full example: [http://editor.md.ipandao.com/examples/html-preview-markdown-to-html.html](http://editor.md.ipandao.com/examples/html-preview-markdown-to-html.html) + +##### HTML to Markdown? + Sorry, Editor.md not support HTML to Markdown parsing, Maybe In the future. #### Examples -[https://pandao.github.io/editor.md/examples/index.html](https://pandao.github.io/editor.md/examples/index.html) - -#### Options - -Editor.md options and default values: - -```javascript +[https://pandao.github.io/editor.md/examples/index.html](https://pandao.github.io/editor.md/examples/index.html) + +#### Options + +Editor.md options and default values: + +```javascript { mode : "gfm", // gfm or markdown name : "", // Form element name for post @@ -229,19 +229,36 @@ Editor.md options and default values: name : "zh-cn", description : "开源在线Markdown编辑器
Open source online Markdown editor.", tocTitle : "目录", - toolbar : { - //... - }, - button: { - //... + toolbar : { + //... + }, + button: { + //... }, - dialog : { - //... - } - //... - } -} + dialog : { + //... + } + //... + } +} +``` + +#### Avoid XSS + +Script and events are disabled by default to avoid XSS + +If you want to enable you need to pass in htmlDecode: + +allowScript as FilterTag +allowOn as FilterAttribute + ``` +htmlDecode : "allowScript|allowOn" +``` + +extra filters can be set in coma separated list format + +Be warned that enabled scripting can be dangerous and lead to [XSS attacks](https://en.wikipedia.org/wiki/Cross-site_scripting) #### Dependents diff --git a/editormd.js b/editormd.js index c33c097a..8689b69f 100644 --- a/editormd.js +++ b/editormd.js @@ -7,7 +7,7 @@ * @license MIT License * @author Pandao * {@link https://github.com/pandao/editor.md} - * @updateTime 2015-06-09 + * @updateTime 2020-09-17 */ ;(function(factory) { @@ -363,9 +363,8 @@ options = id; } - var _this = this; var classPrefix = this.classPrefix = editormd.classPrefix; - var settings = this.settings = $.extend(true, {}, editormd.defaults, options); + var settings = this.settings = $.extend(true, editormd.defaults, options); id = (typeof id === "object") ? settings.id : id; @@ -813,7 +812,6 @@ } var cm = this.cm; - var editor = this.editor; var count = cm.lineCount(); var preview = this.preview; @@ -1112,7 +1110,6 @@ } var editor = this.editor; - var preview = this.preview; var classPrefix = this.classPrefix; var toolbar = this.toolbar = editor.children("." + classPrefix + "toolbar"); @@ -1247,7 +1244,7 @@ var toolbarIcons = this.toolbarIcons = toolbar.find("." + classPrefix + "menu > li > a"); var toolbarIconHandlers = this.getToolbarHandles(); - toolbarIcons.bind(editormd.mouseOrTouch("click", "touchend"), function(event) { + toolbarIcons.bind(editormd.mouseOrTouch("click", "touchend"), function() { var icon = $(this).children(".fa"); var name = icon.attr("name"); @@ -1371,8 +1368,7 @@ $("html,body").css("overflow-x", "hidden"); - var _this = this; - var editor = this.editor; + var editor = this.editor; var settings = this.settings; var infoDialog = this.infoDialog = editor.children("." + this.classPrefix + "dialog-info"); @@ -1434,7 +1430,6 @@ */ recreate : function() { - var _this = this; var editor = this.editor; var settings = this.settings; @@ -1629,18 +1624,15 @@ { case 120: $.proxy(toolbarHandlers["watch"], _this)(); - return false; - break; + return false; case 121: $.proxy(toolbarHandlers["preview"], _this)(); - return false; - break; + return false; case 122: $.proxy(toolbarHandlers["fullscreen"], _this)(); - return false; - break; + return false; default: break; @@ -1965,15 +1957,14 @@ save : function() { - var _this = this; - var state = this.state; - var settings = this.settings; - - if (timer === null && !(!settings.watch && state.preview)) + if (timer === null) { return this; } + var _this = this; + var state = this.state; + var settings = this.settings; var cm = this.cm; var cmValue = cm.getValue(); var previewContainer = this.previewContainer; @@ -3183,17 +3174,20 @@ } }; + var isMac = navigator.platform.toUpperCase().indexOf('MAC')>=0; + var key = isMac ? "Cmd" : "Ctrl"; + editormd.keyMaps = { - "Ctrl-1" : "h1", - "Ctrl-2" : "h2", - "Ctrl-3" : "h3", - "Ctrl-4" : "h4", - "Ctrl-5" : "h5", - "Ctrl-6" : "h6", - "Ctrl-B" : "bold", // if this is string == editormd.toolbarHandlers.xxxx - "Ctrl-D" : "datetime", - - "Ctrl-E" : function() { // emoji + [key + "-1"] : "h1", + [key + "-2"] : "h2", + [key + "-3"] : "h3", + [key + "-4"] : "h4", + [key + "-5"] : "h5", + [key + "-6"] : "h6", + [key + "-B"] : "bold", // if this is string == editormd.toolbarHandlers.xxxx + [key + "-D"] : "datetime", + + [key + "Ctrl-E"] : function() { // emoji var cm = this.cm; var cursor = cm.getCursor(); var selection = cm.getSelection(); @@ -3210,10 +3204,10 @@ cm.setCursor(cursor.line, cursor.ch + 1); } }, - "Ctrl-Alt-G" : "goto-line", - "Ctrl-H" : "hr", - "Ctrl-I" : "italic", - "Ctrl-K" : "code", + [key + "-Alt-G"] : "goto-line", + [key + "-H"] : "hr", + [key + "-I"] : "italic", + [key + "-K"] : "code", "Ctrl-L" : function() { var cm = this.cm; @@ -3228,7 +3222,7 @@ cm.setCursor(cursor.line, cursor.ch + 1); } }, - "Ctrl-U" : "list-ul", + [key + "-U"] : "list-ul", "Shift-Ctrl-A" : function() { var cm = this.cm; @@ -3248,10 +3242,10 @@ } }, - "Shift-Ctrl-C" : "code", - "Shift-Ctrl-Q" : "quote", - "Shift-Ctrl-S" : "del", - "Shift-Ctrl-K" : "tex", // KaTeX + ["Shift" + key + "-C"] : "code", + ["Shift" + key + "Q"] : "quote", + ["Shift" + key + "S"] : "del", + ["Shift" + key + "K"] : "tex", // KaTeX "Shift-Alt-C" : function() { var cm = this.cm; @@ -3265,16 +3259,16 @@ } }, - "Shift-Ctrl-Alt-C" : "code-block", - "Shift-Ctrl-H" : "html-entities", - "Shift-Alt-H" : "help", - "Shift-Ctrl-E" : "emoji", - "Shift-Ctrl-U" : "uppercase", - "Shift-Alt-U" : "ucwords", - "Shift-Ctrl-Alt-U" : "ucfirst", - "Shift-Alt-L" : "lowercase", + ["Shift-" + key + "-Alt-C"] : "code-block", + ["Shift-" + key + "-H"] : "html-entities", + "Shift-Alt-H" : "help", + ["Shift-" + key + "-E"] : "emoji", + ["Shift-" + key + "-U"] : "uppercase", + "Shift-Alt-U" : "ucwords", + ["Shift-" + key + "-Alt-U"] : "ucfirst", + "Shift-Alt-L" : "lowercase", - "Shift-Ctrl-I" : function() { + ["Shift-" + key + "-I"] : function() { var cm = this.cm; var cursor = cm.getCursor(); var selection = cm.getSelection(); @@ -3288,15 +3282,15 @@ } }, - "Shift-Ctrl-Alt-I" : "image", - "Shift-Ctrl-L" : "link", - "Shift-Ctrl-O" : "list-ol", - "Shift-Ctrl-P" : "preformatted-text", - "Shift-Ctrl-T" : "table", - "Shift-Alt-P" : "pagebreak", - "F9" : "watch", - "F10" : "preview", - "F11" : "fullscreen", + ["Shift-" + key + "-Alt-I"] : "image", + ["Shift-" + key + "-L"] : "link", + ["Shift-" + key + "-O"] : "list-ol", + ["Shift-" + key + "-P"] : "preformatted-text", + ["Shift-" + key + "-T"] : "table", + "Shift-Alt-P" : "pagebreak", + "F9" : "watch", + "F10" : "preview", + "F11" : "fullscreen", }; /** @@ -3356,7 +3350,7 @@ email : /(\w+)@(\w+)\.(\w+)\.?(\w+)?/g, emailLink : /(mailto:)?([\w\.\_]+)@(\w+)\.(\w+)\.?(\w+)?/g, emoji : /:([\w\+-]+):/g, - emojiDatetime : /(\d{2}:\d{2}:\d{2})/g, + emojiDatetime : /(\d{1,2}:\d{1,2}:\d{1,2})/g, twemoji : /:(tw-([\w]+)-?(\w+)?):/g, fontAwesome : /:(fa-([\w]+)(-(\w+)){0,}):/g, editormdLogo : /:(editormd-logo-?(\w+)?):/g, @@ -3365,7 +3359,7 @@ // Emoji graphics files url path editormd.emoji = { - path : "https://www.webpagefx.com/tools/emoji-cheat-sheet/graphics/emojis/", + path : "http://www.emoji-cheat-sheet.com/graphics/emojis/", ext : ".png" }; @@ -3808,68 +3802,126 @@ */ editormd.filterHTMLTags = function(html, filters) { - + + const basicAttrs ={ + 'img': 'src', + 'a': 'href' + } + if (typeof html !== "string") { html = new String(html); } - + + try{ + html = decodeURI(html) + }catch(error){ + return "Invalid encoding detected" + } if (typeof filters !== "string") { - return html; + // If no filters set use "script|on*" by default to avoid XSS + filters = "script|on*"; } var expression = filters.split("|"); var filterTags = expression[0].split(","); var attrs = expression[1]; + if(!filterTags.includes('allowScript') && !filterTags.includes('script')) + { + // Only allow script if requested specifically + filterTags.push('script'); + } + for (var i = 0, len = filterTags.length; i < len; i++) { var tag = filterTags[i]; - html = html.replace(new RegExp("\<\s*" + tag + "\s*([^\>]*)\>([^\>]*)\<\s*\/" + tag + "\s*\>", "igm"), ""); + html = html.replace(new RegExp("\<\s*" + tag + "\s*([^\>]*)\>(?:([^\>]*)\<\s*\/" + tag + "\s*\>)?", "igm"), ""); + } //return html; + if (typeof attrs === "undefined") + { + // If no attrs set, block "on*" to avoid XSS + attrs = "on*" + } + if (typeof attrs !== "undefined") { - var htmlTagRegex = /\<(\w+)\s*([^\>]*)\>([^\>]*)\<\/(\w+)\>/ig; + var htmlTagRegex = /\<(\w+)\s*([^\>]*)\>(?:([^\>]*)\<\/(\1)\>)?/ig; + + var filterAttrs = attrs.split(","); + var filterOn = true; + + if(filterAttrs.includes('allowOn')) + { + // Only allow on* if requested specifically + filterOn = false; + } if (attrs === "*") { html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) { - return "<" + $2 + ">" + $4 + ""; - }); + // Add basic attrs to elements that need them + Object.entries(basicAttrs).forEach( item => + { + var match; + if(item[0].toUpperCase() === $2.toUpperCase()) + { + var regBas = new RegExp(item[1]+`\s*=\s*("|')(?:(?!\\1).)*\\1`,"i"); + if(match = regBas.exec($3)){ + $2 += ' ' + match[0]; + } + } + }); + if(typeof($4)!== 'undefined'){ + return "<" + $2 + ">" + $4 + ""; + }else{ + return "<" + $2 + "/>"; + } + }); } - else if (attrs === "on*") + else if ((attrs === "on*") || filterOn) { + html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) { - var el = $("<" + $2 + ">" + $4 + ""); - var _attrs = $($1)[0].attributes; + var el; + try{ + if(typeof($4)!== 'undefined'){ + el = $("<" + $2 + ">" + $4 + ""); + }else{ + el = $("<" + $2 + "/>"); + } + } catch (error){ + console.log('Trying to create invalid element'); + return ''; + } +// var _attrs = $($1)[0].attributes; // ARH: Replace with regexp, beacause this triggers execution of onLoad ... (Also should be faster now) + var match; var $attrs = {}; - - $.each(_attrs, function(i, e) { - if (e.nodeName !== '"') $attrs[e.nodeName] = e.nodeValue; - }); - - $.each($attrs, function(i) { - if (i.indexOf("on") === 0) { - delete $attrs[i]; + var regOn = /^on*/i + + var regAttr = /(\w*)\s*=\s*("|')((?:(?!\2).)*)\2/gi; + while(match = regAttr.exec($3)){ + if (!regOn.exec(match[1]) && match[1].length>0){ + $attrs[match[1]] = match[3]; } - }); - + } el.attr($attrs); var text = (typeof el[1] !== "undefined") ? $(el[1]).text() : ""; - return el[0].outerHTML + text; }); } - else + if(filterAttrs.length > 1 || (filterAttrs[0]!=="*" && filterAttrs[0]!=="on*")) { html = html.replace(htmlTagRegex, function($1, $2, $3, $4) { - var filterAttrs = attrs.split(","); var el = $($1); - el.html($4); + if(typeof($4)!== 'undefined'){ + el.html($4); + } $.each(filterAttrs, function(i) { el.attr(filterAttrs[i], null); diff --git a/src/editormd.js b/src/editormd.js index bf4f7f17..55f9e37e 100644 --- a/src/editormd.js +++ b/src/editormd.js @@ -351,7 +351,6 @@ options = id; } - var _this = this; var classPrefix = this.classPrefix = editormd.classPrefix; var settings = this.settings = $.extend(true, editormd.defaults, options); @@ -801,7 +800,6 @@ } var cm = this.cm; - var editor = this.editor; var count = cm.lineCount(); var preview = this.preview; @@ -1100,7 +1098,6 @@ } var editor = this.editor; - var preview = this.preview; var classPrefix = this.classPrefix; var toolbar = this.toolbar = editor.children("." + classPrefix + "toolbar"); @@ -1235,7 +1232,7 @@ var toolbarIcons = this.toolbarIcons = toolbar.find("." + classPrefix + "menu > li > a"); var toolbarIconHandlers = this.getToolbarHandles(); - toolbarIcons.bind(editormd.mouseOrTouch("click", "touchend"), function(event) { + toolbarIcons.bind(editormd.mouseOrTouch("click", "touchend"), function() { var icon = $(this).children(".fa"); var name = icon.attr("name"); @@ -1359,8 +1356,7 @@ $("html,body").css("overflow-x", "hidden"); - var _this = this; - var editor = this.editor; + var editor = this.editor; var settings = this.settings; var infoDialog = this.infoDialog = editor.children("." + this.classPrefix + "dialog-info"); @@ -1422,7 +1418,6 @@ */ recreate : function() { - var _this = this; var editor = this.editor; var settings = this.settings; @@ -1617,18 +1612,15 @@ { case 120: $.proxy(toolbarHandlers["watch"], _this)(); - return false; - break; + return false; case 121: $.proxy(toolbarHandlers["preview"], _this)(); - return false; - break; + return false; case 122: $.proxy(toolbarHandlers["fullscreen"], _this)(); - return false; - break; + return false; default: break; @@ -3798,68 +3790,126 @@ */ editormd.filterHTMLTags = function(html, filters) { - + + const basicAttrs ={ + 'img': 'src', + 'a': 'href' + } + if (typeof html !== "string") { html = new String(html); } - + + try{ + html = decodeURI(html) + }catch(error){ + return "Invalid encoding detected" + } if (typeof filters !== "string") { - return html; + // If no filters set use "script|on*" by default to avoid XSS + filters = "script|on*"; } var expression = filters.split("|"); var filterTags = expression[0].split(","); var attrs = expression[1]; + if(!filterTags.includes('allowScript') && !filterTags.includes('script')) + { + // Only allow script if requested specifically + filterTags.push('script'); + } + for (var i = 0, len = filterTags.length; i < len; i++) { var tag = filterTags[i]; - html = html.replace(new RegExp("\<\s*" + tag + "\s*([^\>]*)\>([^\>]*)\<\s*\/" + tag + "\s*\>", "igm"), ""); + html = html.replace(new RegExp("\<\s*" + tag + "\s*([^\>]*)\>(?:([^\>]*)\<\s*\/" + tag + "\s*\>)?", "igm"), ""); + } //return html; + if (typeof attrs === "undefined") + { + // If no attrs set, block "on*" to avoid XSS + attrs = "on*" + } + if (typeof attrs !== "undefined") { - var htmlTagRegex = /\<(\w+)\s*([^\>]*)\>([^\>]*)\<\/(\w+)\>/ig; + var htmlTagRegex = /\<(\w+)\s*([^\>]*)\>(?:([^\>]*)\<\/(\1)\>)?/ig; + + var filterAttrs = attrs.split(","); + var filterOn = true; + + if(filterAttrs.includes('allowOn')) + { + // Only allow on* if requested specifically + filterOn = false; + } if (attrs === "*") { html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) { - return "<" + $2 + ">" + $4 + ""; - }); + // Add basic attrs to elements that need them + Object.entries(basicAttrs).forEach( item => + { + var match; + if(item[0].toUpperCase() === $2.toUpperCase()) + { + var regBas = new RegExp(item[1]+`\s*=\s*("|')(?:(?!\\1).)*\\1`,"i"); + if(match = regBas.exec($3)){ + $2 += ' ' + match[0]; + } + } + }); + if(typeof($4)!== 'undefined'){ + return "<" + $2 + ">" + $4 + ""; + }else{ + return "<" + $2 + "/>"; + } + }); } - else if (attrs === "on*") + else if ((attrs === "on*") || filterOn) { + html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) { - var el = $("<" + $2 + ">" + $4 + ""); - var _attrs = $($1)[0].attributes; + var el; + try{ + if(typeof($4)!== 'undefined'){ + el = $("<" + $2 + ">" + $4 + ""); + }else{ + el = $("<" + $2 + "/>"); + } + } catch (error){ + console.log('Trying to create invalid element'); + return ''; + } +// var _attrs = $($1)[0].attributes; // ARH: Replace with regexp, beacause this triggers execution of onLoad ... (Also should be faster now) + var match; var $attrs = {}; - - $.each(_attrs, function(i, e) { - if (e.nodeName !== '"') $attrs[e.nodeName] = e.nodeValue; - }); - - $.each($attrs, function(i) { - if (i.indexOf("on") === 0) { - delete $attrs[i]; + var regOn = /^on*/i + + var regAttr = /(\w*)\s*=\s*("|')((?:(?!\2).)*)\2/gi; + while(match = regAttr.exec($3)){ + if (!regOn.exec(match[1]) && match[1].length>0){ + $attrs[match[1]] = match[3]; } - }); - + } el.attr($attrs); var text = (typeof el[1] !== "undefined") ? $(el[1]).text() : ""; - return el[0].outerHTML + text; }); } - else + if(filterAttrs.length > 1 || (filterAttrs[0]!=="*" && filterAttrs[0]!=="on*")) { html = html.replace(htmlTagRegex, function($1, $2, $3, $4) { - var filterAttrs = attrs.split(","); var el = $($1); - el.html($4); + if(typeof($4)!== 'undefined'){ + el.html($4); + } $.each(filterAttrs, function(i) { el.attr(filterAttrs[i], null);