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 + "" + $5 + ">";
- });
+ // 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 + "" + $5 + ">";
+ }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 + "" + $5 + ">");
- var _attrs = $($1)[0].attributes;
+ var el;
+ try{
+ if(typeof($4)!== 'undefined'){
+ el = $("<" + $2 + ">" + $4 + "" + $5 + ">");
+ }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 + "" + $5 + ">";
- });
+ // 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 + "" + $5 + ">";
+ }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 + "" + $5 + ">");
- var _attrs = $($1)[0].attributes;
+ var el;
+ try{
+ if(typeof($4)!== 'undefined'){
+ el = $("<" + $2 + ">" + $4 + "" + $5 + ">");
+ }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);