Skip to content

Commit

Permalink
fix: Add sanitizer for filtering HTML tags (hinesboy#744)
Browse files Browse the repository at this point in the history
* fix: Add sanitizer for filtering HTML tags

* fix: Do not share `markdown-it` instances

* chore: fix lint

Co-authored-by: wangsongc <[email protected]>
  • Loading branch information
jiawulin001 and wangsongc authored Dec 14, 2021
1 parent 8a2eb2a commit b9489a3
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 147 deletions.
3 changes: 2 additions & 1 deletion README-EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ export default {
| imageFilter | Function | null | Image file filter Function, params is a `File Object`, you should return `Boolean` about the test result |
| imageClick | function | null | Image Click Function |
| tabSize | Number | null | How many spaces equals one tab, default \t |
| xssOptions | Object | {} | xss rule configuration, enabled by default, set to false to turn off, custom rule reference [https://jsxss.com/zh/options.html](https://jsxss.com/zh/options.html) |
| html | Boolean | true | Enable HTML tags in source, for historical reasons this tag has always been true by default, but it is recommended to turn it off if you don't need this feature, as doing so it eliminates the security vulnerabilities altogether. |
| xssOptions | Object | {} | xss rules configuration, enabled by default, set to false to turn off, enabled will filter HTML tags, the default filter all HTML tag attributes, it is recommended to configure the whitelist on demand to reduce the possibility of being attacked.<br/> - custom rule reference: [https://jsxss.com/zh/options.html](https://jsxss.com/zh/options.html)<br/>- Demo: [dev-demo](./src/dev/editor.vue) |
| toolbars | Object | As in the following example | toolbars |

#### toolbars
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ export default {
| imageFilter | function | null | 图片过滤函数,参数为一个`File Object`,要求返回一个`Boolean`, `true`表示文件合法,`false`表示文件不合法 |
| imageClick | function | null | 图片点击事件,默认为预览,可覆盖 |
| tabSize | Number | \t | tab转化为几个空格,默认为\t |
| xssOptions | Object | {} | xss规则配置, 默认开启,设置false可以关闭,自定义规则参考 [https://jsxss.com/zh/options.html](https://jsxss.com/zh/options.html) |
| html | Boolean | true | 启用HTML标签,因为历史原因这个标记一直默认为true,但建议不使用HTML标签就关闭它,它能彻底杜绝安全问题。 |
| xssOptions | Object | {} | xss规则配置, 默认开启,设置false可以关闭,开启后会对HTML标签进行过滤,默认过滤所有HTML标签属性,建议按需配置白名单减少被攻击的可能。<br/>- 自定义规则参考: [https://jsxss.com/zh/options.html](https://jsxss.com/zh/options.html)<br/>- 参考DEMO: [dev-demo](./src/dev/editor.vue) |
| toolbars | Object | 如下例 | 工具栏 |

#### toolbars
Expand Down
9 changes: 7 additions & 2 deletions src/dev/editor.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="container">
<div id="editor">
<mavon-editor style="height: 100%" v-model="code" :codeStyle="codeStyle"></mavon-editor>
<mavon-editor style="height: 100%" v-model="code" :codeStyle="codeStyle" :xssOptions="xssOptions"></mavon-editor>
</div>
<div class="switch-code-style">
<span>code style:</span>
Expand Down Expand Up @@ -38,7 +38,12 @@ module.exports = {
return {
codeStyle: "github",
styles,
code: '```' + code + '\n```'
code: '<span style="color:red;font-size:36px;">A</span> \n```' + code + '\n```',
xssOptions:{
whiteList: {
span: ['style']
}
}
};
}
}
Expand Down
34 changes: 0 additions & 34 deletions src/lib/core/rules.js

This file was deleted.

31 changes: 31 additions & 0 deletions src/lib/core/sanitizer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FilterXSS } from 'xss';

let xssHandler;

function mavoneditor_sanitizer(state) {
if (!xssHandler) {
return;
}
sanitizer(state.tokens, ['inline', 'html_block']);
}

function sanitizer(tokens, types) {
let originContent, children;
for (let i = 0; i < tokens.length; i++) {
if (types.indexOf(tokens[i].type) !== -1) {
originContent = tokens[i].content;
children = tokens[i].children;
tokens[i].content = xssHandler.process(originContent);
if (children && children.length && originContent !== tokens[i].content) {
sanitizer(children, ['html_inline']);
}
}
}
}

export default function (md, xssOptions) {
if (md.options.html) {
xssHandler = new FilterXSS(xssOptions);
md.core.ruler.push('mavoneditor_sanitizer', mavoneditor_sanitizer);
}
}
112 changes: 61 additions & 51 deletions src/lib/mixins/markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import hljsLangs from '../core/hljs/lang.hljs.js'
import {
loadScript
} from '../core/extra-function.js'
import { headRule, imageRule } from '../core/rules.js'
import sanitizer from '../core/sanitizer.js'

var markdown_config = {
html: true, // Enable HTML tags in source
Expand All @@ -14,7 +14,7 @@ var markdown_config = {
quotes: '“”‘’'
}

var markdown = require('markdown-it')(markdown_config);
var MarkdownIt = require('markdown-it');
// 表情
var emoji = require('markdown-it-emoji');
// 下标
Expand All @@ -37,28 +37,6 @@ var taskLists = require('markdown-it-task-lists')
var container = require('markdown-it-container')
//
var toc = require('markdown-it-toc')
// add target="_blank" to all link
var defaultRender = markdown.renderer.rules.link_open || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
markdown.renderer.rules.link_open = function (tokens, idx, options, env, self) {
var hIndex = tokens[idx].attrIndex('href');
if (tokens[idx].attrs[hIndex][1].startsWith('#')) return defaultRender(tokens, idx, options, env, self);
// If you are sure other plugins can't add `target` - drop check below
var aIndex = tokens[idx].attrIndex('target');

if (aIndex < 0) {
tokens[idx].attrPush(['target', '_blank']); // add new attribute
} else {
tokens[idx].attrs[aIndex][1] = '_blank'; // replace value of existing attr
}

// pass token to default renderer.
return defaultRender(tokens, idx, options, env, self);
};

const defautlImageRule = markdown.renderer.rules.image;
markdown.renderer.rules.image = imageRule(defautlImageRule);

var mihe = require('markdown-it-highlightjs-external');
// math katex
Expand All @@ -69,51 +47,83 @@ var needLangs = [];
var hljs_opts = {
hljs: 'auto',
highlighted: true,
langCheck: function(lang) {
langCheck: function (lang) {
if (lang && hljsLangs[lang] && !missLangs[lang]) {
missLangs[lang] = 1;
needLangs.push(hljsLangs[lang])
}
}
};
markdown.use(mihe, hljs_opts)
.use(emoji)
.use(sup)
.use(sub)
.use(container)
.use(container, 'hljs-left') /* align left */
.use(container, 'hljs-center')/* align center */
.use(container, 'hljs-right')/* align right */
.use(deflist)
.use(abbr)
.use(footnote)
.use(insert)
.use(mark)
.use(container)
.use(miip)
.use(katex)
.use(taskLists)
.use(toc)

const tocHeadRule = markdown.renderer.rules.heading_open;
markdown.renderer.rules.heading_open = headRule(tocHeadRule);
function initMarkdown() {
const markdown = new MarkdownIt(markdown_config);

// add target="_blank" to all link
var defaultRender = markdown.renderer.rules.link_open || function (tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
markdown.renderer.rules.link_open = function (tokens, idx, options, env, self) {
var hIndex = tokens[idx].attrIndex('href');
if (tokens[idx].attrs[hIndex][1].startsWith('#')) return defaultRender(tokens, idx, options, env, self);
// If you are sure other plugins can't add `target` - drop check below
var aIndex = tokens[idx].attrIndex('target');

if (aIndex < 0) {
tokens[idx].attrPush(['target', '_blank']); // add new attribute
} else {
tokens[idx].attrs[aIndex][1] = '_blank'; // replace value of existing attr
}

// pass token to default renderer.
return defaultRender(tokens, idx, options, env, self);
};

markdown.use(mihe, hljs_opts)
.use(emoji)
.use(sup)
.use(sub)
.use(container)
.use(container, 'hljs-left') /* align left */
.use(container, 'hljs-center')/* align center */
.use(container, 'hljs-right')/* align right */
.use(deflist)
.use(abbr)
.use(footnote)
.use(insert)
.use(mark)
.use(container)
.use(miip)
.use(katex)
.use(taskLists)
.use(toc)

return markdown;
}

export default {
data() {
return {
markdownIt: markdown
MarkdownIt: null
}
},
created() {
this.MarkdownIt = initMarkdown();
if (!this.html) {
this.MarkdownIt.set({ html: false });
this.xssOptions = false;
} else if (typeof this.xssOptions === 'object') {
this.MarkdownIt.use(sanitizer, this.xssOptions);
}
},
mounted() {
var $vm = this;
hljs_opts.highlighted = this.ishljs;
},
methods: {
$render(src, func) {
var $vm = this;
missLangs = {};
needLangs = [];
var res = markdown.render(src);
var res = this.MarkdownIt.render(src);
if (this.ishljs) {
if (needLangs.length > 0) {
$vm.$_render(src, func, res);
Expand All @@ -126,18 +136,18 @@ export default {
var deal = 0;
for (var i = 0; i < needLangs.length; i++) {
var url = $vm.p_external_link.hljs_lang(needLangs[i]);
loadScript(url, function() {
loadScript(url, function () {
deal = deal + 1;
if (deal === needLangs.length) {
res = markdown.render(src);
res = this.MarkdownIt.render(src);
func(res);
}
})
}
}
},
watch: {
ishljs: function(val) {
ishljs: function (val) {
hljs_opts.highlighted = val;
}
}
Expand Down
62 changes: 4 additions & 58 deletions src/mavon-editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@ import md_toolbar_left from './components/md-toolbar-left.vue'
import md_toolbar_right from './components/md-toolbar-right.vue'
import "./lib/font/css/fontello.css"
import './lib/css/md.css'
import { skipRule } from './lib/core/rules.js'
import { FilterXSS } from 'xss';
export default {
mixins: [markdown],
Expand Down Expand Up @@ -199,6 +197,10 @@ export default {
return CONFIG.toolbars
}
},
html: {// Enable HTML tags in source
type: Boolean,
default: true
},
xssOptions: { // XSS 选项
type: [Object, Boolean],
default() {
Expand Down Expand Up @@ -643,65 +645,9 @@ export default {
console.warn('hljs color scheme', val, 'do not exist, hljs color scheme will not change');
}
},
xssHandler(htmlCode) {
if (this._xssHandler) {
return this._xssHandler.process(htmlCode);
}
let originalTagFun;
if (typeof this.xssOptions['onTag'] === 'function') {
originalTagFun = this.xssOptions['onTag'];
}
this.xssOptions['onTag'] = function(tag, html, info) {
let code = skipRule(tag, html);
if (originalTagFun) {
code = originalTagFun(tag,code);
}
if (html !== code) {
return code;
}
}
let originalTagAttr;
if (typeof this.xssOptions['onTagAttr'] === 'function') {
originalTagAttr = this.xssOptions['onTagAttr'];
}
this.xssOptions['onTagAttr'] = function (tag, name, value) {
const whiteClass = {
"div": ['hljs-left', 'hljs-center', 'hljs-right', 'hljs-*'],
"code": ['lang-language','lang-*'],
"span": ['hljs-*']
};
let newValue, oriValue;
if (name === 'class' &&
whiteClass[tag] &&
whiteClass[tag].find(el => {
return !!value.match(el)
}))
{
newValue = name + '="' + value + '"';
}
if (originalTagAttr) {
oriValue = originalTagAttr(tag, name, value);
}
if (newValue || oriValue) {
return oriValue || newValue;
}
};
this._xssHandler = new FilterXSS(this.xssOptions);
return this._xssHandler.process(htmlCode);
},
iRender(toggleChange) {
var $vm = this;
this.$render($vm.d_value, function(res) {
// HTML 渲染前先进行过滤,避免 xss 问题,默认情况下开始此功能
if (typeof $vm.xssOptions === 'object') {
res = $vm.xssHandler(res);
}
$vm.d_render = res;
// change回调 toggleChange == false 时候触发change回调
if (!toggleChange)
Expand Down

0 comments on commit b9489a3

Please sign in to comment.