You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					230 lines
				
				5.7 KiB
			
		
		
			
		
	
	
					230 lines
				
				5.7 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * 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;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								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 "</" + 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()`
							 | 
						||
| 
								 | 
							
								            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;
							 |