/** * filter xss * * @author Zongmin Lei */ 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; } function keysToLowerCase(obj) { var ret = {}; for (var i in obj) { if (Array.isArray(obj[i])) { ret[i.toLowerCase()] = obj[i].map(function (item) { return item.toLowerCase(); }); } else { ret[i.toLowerCase()] = obj[i]; } } return ret; } /** * FilterXSS class * * @param {Object} options * whiteList (or allowList), 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; } if (options.whiteList || options.allowList) { options.whiteList = keysToLowerCase(options.whiteList || options.allowList); } else { 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) { 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: Object.prototype.hasOwnProperty.call(whiteList, tag), }; // call `onTag()` var ret = onTag(tag, html, info); if (!isNull(ret)) return ret; if (info.isWhite) { if (info.isClosing) { return ""; } 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()` ret = onIgnoreTagAttr(tag, name, value, isWhiteAttr); if (!isNull(ret)) return ret; return; } }); // build new tag html html = "<" + tag; if (attrsHtml) html += " " + attrsHtml; if (attrs.closing) html += " /"; html += ">"; return html; } else { // call `onIgnoreTag()` 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;