212 lines
5.3 KiB
JavaScript
212 lines
5.3 KiB
JavaScript
|
/**
|
||
|
* filter xss
|
||
|
*
|
||
|
* @author Zongmin Lei<leizongmin@gmail.com>
|
||
|
*/
|
||
|
|
||
|
var FilterCSS = require("cssfilter").FilterCSS;
|
||
|
var DEFAULT = require("./default");
|
||
|
var parser = require("./parser");
|
||
|
var parseTag = parser.parseTag;
|
||
|
var parseAttr = parser.parseAttr;
|
||
|
var _ = require("./util");
|
||
|
|
||
|
/**
|
||
|
* returns `true` if the input value is `undefined` or `null`
|
||
|
*
|
||
|
* @param {Object} obj
|
||
|
* @return {Boolean}
|
||
|
*/
|
||
|
function isNull(obj) {
|
||
|
return obj === undefined || obj === null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* get attributes for a tag
|
||
|
*
|
||
|
* @param {String} html
|
||
|
* @return {Object}
|
||
|
* - {String} html
|
||
|
* - {Boolean} closing
|
||
|
*/
|
||
|
function getAttrs(html) {
|
||
|
var i = _.spaceIndex(html);
|
||
|
if (i === -1) {
|
||
|
return {
|
||
|
html: "",
|
||
|
closing: html[html.length - 2] === "/",
|
||
|
};
|
||
|
}
|
||
|
html = _.trim(html.slice(i + 1, -1));
|
||
|
var isClosing = html[html.length - 1] === "/";
|
||
|
if (isClosing) html = _.trim(html.slice(0, -1));
|
||
|
return {
|
||
|
html: html,
|
||
|
closing: isClosing,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* shallow copy
|
||
|
*
|
||
|
* @param {Object} obj
|
||
|
* @return {Object}
|
||
|
*/
|
||
|
function shallowCopyObject(obj) {
|
||
|
var ret = {};
|
||
|
for (var i in obj) {
|
||
|
ret[i] = obj[i];
|
||
|
}
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* FilterXSS class
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* whiteList, onTag, onTagAttr, onIgnoreTag,
|
||
|
* onIgnoreTagAttr, safeAttrValue, escapeHtml
|
||
|
* stripIgnoreTagBody, allowCommentTag, stripBlankChar
|
||
|
* css{whiteList, onAttr, onIgnoreAttr} `css=false` means don't use `cssfilter`
|
||
|
*/
|
||
|
function FilterXSS(options) {
|
||
|
options = shallowCopyObject(options || {});
|
||
|
|
||
|
if (options.stripIgnoreTag) {
|
||
|
if (options.onIgnoreTag) {
|
||
|
console.error(
|
||
|
'Notes: cannot use these two options "stripIgnoreTag" and "onIgnoreTag" at the same time'
|
||
|
);
|
||
|
}
|
||
|
options.onIgnoreTag = DEFAULT.onIgnoreTagStripAll;
|
||
|
}
|
||
|
|
||
|
options.whiteList = options.whiteList || DEFAULT.whiteList;
|
||
|
options.onTag = options.onTag || DEFAULT.onTag;
|
||
|
options.onTagAttr = options.onTagAttr || DEFAULT.onTagAttr;
|
||
|
options.onIgnoreTag = options.onIgnoreTag || DEFAULT.onIgnoreTag;
|
||
|
options.onIgnoreTagAttr = options.onIgnoreTagAttr || DEFAULT.onIgnoreTagAttr;
|
||
|
options.safeAttrValue = options.safeAttrValue || DEFAULT.safeAttrValue;
|
||
|
options.escapeHtml = options.escapeHtml || DEFAULT.escapeHtml;
|
||
|
this.options = options;
|
||
|
|
||
|
if (options.css === false) {
|
||
|
this.cssFilter = false;
|
||
|
} else {
|
||
|
options.css = options.css || {};
|
||
|
this.cssFilter = new FilterCSS(options.css);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* start process and returns result
|
||
|
*
|
||
|
* @param {String} html
|
||
|
* @return {String}
|
||
|
*/
|
||
|
FilterXSS.prototype.process = function (html) {
|
||
|
// compatible with the input
|
||
|
html = html || "";
|
||
|
html = html.toString();
|
||
|
if (!html) return "";
|
||
|
|
||
|
var me = this;
|
||
|
var options = me.options;
|
||
|
var whiteList = options.whiteList;
|
||
|
var onTag = options.onTag;
|
||
|
var onIgnoreTag = options.onIgnoreTag;
|
||
|
var onTagAttr = options.onTagAttr;
|
||
|
var onIgnoreTagAttr = options.onIgnoreTagAttr;
|
||
|
var safeAttrValue = options.safeAttrValue;
|
||
|
var escapeHtml = options.escapeHtml;
|
||
|
var cssFilter = me.cssFilter;
|
||
|
|
||
|
// remove invisible characters
|
||
|
if (options.stripBlankChar) {
|
||
|
html = DEFAULT.stripBlankChar(html);
|
||
|
}
|
||
|
|
||
|
// remove html comments
|
||
|
if (!options.allowCommentTag) {
|
||
|
html = DEFAULT.stripCommentTag(html);
|
||
|
}
|
||
|
|
||
|
// if enable stripIgnoreTagBody
|
||
|
var stripIgnoreTagBody = false;
|
||
|
if (options.stripIgnoreTagBody) {
|
||
|
var stripIgnoreTagBody = DEFAULT.StripTagBody(
|
||
|
options.stripIgnoreTagBody,
|
||
|
onIgnoreTag
|
||
|
);
|
||
|
onIgnoreTag = stripIgnoreTagBody.onIgnoreTag;
|
||
|
}
|
||
|
|
||
|
var retHtml = parseTag(
|
||
|
html,
|
||
|
function (sourcePosition, position, tag, html, isClosing) {
|
||
|
var info = {
|
||
|
sourcePosition: sourcePosition,
|
||
|
position: position,
|
||
|
isClosing: isClosing,
|
||
|
isWhite: whiteList.hasOwnProperty(tag),
|
||
|
};
|
||
|
|
||
|
// call `onTag()`
|
||
|
var ret = onTag(tag, html, info);
|
||
|
if (!isNull(ret)) return ret;
|
||
|
|
||
|
if (info.isWhite) {
|
||
|
if (info.isClosing) {
|
||
|
return "</" + tag + ">";
|
||
|
}
|
||
|
|
||
|
var attrs = getAttrs(html);
|
||
|
var whiteAttrList = whiteList[tag];
|
||
|
var attrsHtml = parseAttr(attrs.html, function (name, value) {
|
||
|
// call `onTagAttr()`
|
||
|
var isWhiteAttr = _.indexOf(whiteAttrList, name) !== -1;
|
||
|
var ret = onTagAttr(tag, name, value, isWhiteAttr);
|
||
|
if (!isNull(ret)) return ret;
|
||
|
|
||
|
if (isWhiteAttr) {
|
||
|
// call `safeAttrValue()`
|
||
|
value = safeAttrValue(tag, name, value, cssFilter);
|
||
|
if (value) {
|
||
|
return name + '="' + value + '"';
|
||
|
} else {
|
||
|
return name;
|
||
|
}
|
||
|
} else {
|
||
|
// call `onIgnoreTagAttr()`
|
||
|
var ret = onIgnoreTagAttr(tag, name, value, isWhiteAttr);
|
||
|
if (!isNull(ret)) return ret;
|
||
|
return;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// build new tag html
|
||
|
var html = "<" + tag;
|
||
|
if (attrsHtml) html += " " + attrsHtml;
|
||
|
if (attrs.closing) html += " /";
|
||
|
html += ">";
|
||
|
return html;
|
||
|
} else {
|
||
|
// call `onIgnoreTag()`
|
||
|
var ret = onIgnoreTag(tag, html, info);
|
||
|
if (!isNull(ret)) return ret;
|
||
|
return escapeHtml(html);
|
||
|
}
|
||
|
},
|
||
|
escapeHtml
|
||
|
);
|
||
|
|
||
|
// if enable stripIgnoreTagBody
|
||
|
if (stripIgnoreTagBody) {
|
||
|
retHtml = stripIgnoreTagBody.remove(retHtml);
|
||
|
}
|
||
|
|
||
|
return retHtml;
|
||
|
};
|
||
|
|
||
|
module.exports = FilterXSS;
|