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; |