xss.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. /**
  2. * filter xss
  3. *
  4. * @author Zongmin Lei<leizongmin@gmail.com>
  5. */
  6. var FilterCSS = require("cssfilter").FilterCSS;
  7. var DEFAULT = require("./default");
  8. var parser = require("./parser");
  9. var parseTag = parser.parseTag;
  10. var parseAttr = parser.parseAttr;
  11. var _ = require("./util");
  12. /**
  13. * returns `true` if the input value is `undefined` or `null`
  14. *
  15. * @param {Object} obj
  16. * @return {Boolean}
  17. */
  18. function isNull(obj) {
  19. return obj === undefined || obj === null;
  20. }
  21. /**
  22. * get attributes for a tag
  23. *
  24. * @param {String} html
  25. * @return {Object}
  26. * - {String} html
  27. * - {Boolean} closing
  28. */
  29. function getAttrs(html) {
  30. var i = _.spaceIndex(html);
  31. if (i === -1) {
  32. return {
  33. html: "",
  34. closing: html[html.length - 2] === "/",
  35. };
  36. }
  37. html = _.trim(html.slice(i + 1, -1));
  38. var isClosing = html[html.length - 1] === "/";
  39. if (isClosing) html = _.trim(html.slice(0, -1));
  40. return {
  41. html: html,
  42. closing: isClosing,
  43. };
  44. }
  45. /**
  46. * shallow copy
  47. *
  48. * @param {Object} obj
  49. * @return {Object}
  50. */
  51. function shallowCopyObject(obj) {
  52. var ret = {};
  53. for (var i in obj) {
  54. ret[i] = obj[i];
  55. }
  56. return ret;
  57. }
  58. function keysToLowerCase(obj) {
  59. var ret = {};
  60. for (var i in obj) {
  61. if (Array.isArray(obj[i])) {
  62. ret[i.toLowerCase()] = obj[i].map(function (item) {
  63. return item.toLowerCase();
  64. });
  65. } else {
  66. ret[i.toLowerCase()] = obj[i];
  67. }
  68. }
  69. return ret;
  70. }
  71. /**
  72. * FilterXSS class
  73. *
  74. * @param {Object} options
  75. * whiteList (or allowList), onTag, onTagAttr, onIgnoreTag,
  76. * onIgnoreTagAttr, safeAttrValue, escapeHtml
  77. * stripIgnoreTagBody, allowCommentTag, stripBlankChar
  78. * css{whiteList, onAttr, onIgnoreAttr} `css=false` means don't use `cssfilter`
  79. */
  80. function FilterXSS(options) {
  81. options = shallowCopyObject(options || {});
  82. if (options.stripIgnoreTag) {
  83. if (options.onIgnoreTag) {
  84. console.error(
  85. 'Notes: cannot use these two options "stripIgnoreTag" and "onIgnoreTag" at the same time'
  86. );
  87. }
  88. options.onIgnoreTag = DEFAULT.onIgnoreTagStripAll;
  89. }
  90. if (options.whiteList || options.allowList) {
  91. options.whiteList = keysToLowerCase(options.whiteList || options.allowList);
  92. } else {
  93. options.whiteList = DEFAULT.whiteList;
  94. }
  95. this.attributeWrapSign = options.singleQuotedAttributeValue === true ? "'" : DEFAULT.attributeWrapSign;
  96. options.onTag = options.onTag || DEFAULT.onTag;
  97. options.onTagAttr = options.onTagAttr || DEFAULT.onTagAttr;
  98. options.onIgnoreTag = options.onIgnoreTag || DEFAULT.onIgnoreTag;
  99. options.onIgnoreTagAttr = options.onIgnoreTagAttr || DEFAULT.onIgnoreTagAttr;
  100. options.safeAttrValue = options.safeAttrValue || DEFAULT.safeAttrValue;
  101. options.escapeHtml = options.escapeHtml || DEFAULT.escapeHtml;
  102. this.options = options;
  103. if (options.css === false) {
  104. this.cssFilter = false;
  105. } else {
  106. options.css = options.css || {};
  107. this.cssFilter = new FilterCSS(options.css);
  108. }
  109. }
  110. /**
  111. * start process and returns result
  112. *
  113. * @param {String} html
  114. * @return {String}
  115. */
  116. FilterXSS.prototype.process = function (html) {
  117. // compatible with the input
  118. html = html || "";
  119. html = html.toString();
  120. if (!html) return "";
  121. var me = this;
  122. var options = me.options;
  123. var whiteList = options.whiteList;
  124. var onTag = options.onTag;
  125. var onIgnoreTag = options.onIgnoreTag;
  126. var onTagAttr = options.onTagAttr;
  127. var onIgnoreTagAttr = options.onIgnoreTagAttr;
  128. var safeAttrValue = options.safeAttrValue;
  129. var escapeHtml = options.escapeHtml;
  130. var attributeWrapSign = me.attributeWrapSign;
  131. var cssFilter = me.cssFilter;
  132. // remove invisible characters
  133. if (options.stripBlankChar) {
  134. html = DEFAULT.stripBlankChar(html);
  135. }
  136. // remove html comments
  137. if (!options.allowCommentTag) {
  138. html = DEFAULT.stripCommentTag(html);
  139. }
  140. // if enable stripIgnoreTagBody
  141. var stripIgnoreTagBody = false;
  142. if (options.stripIgnoreTagBody) {
  143. stripIgnoreTagBody = DEFAULT.StripTagBody(
  144. options.stripIgnoreTagBody,
  145. onIgnoreTag
  146. );
  147. onIgnoreTag = stripIgnoreTagBody.onIgnoreTag;
  148. }
  149. var retHtml = parseTag(
  150. html,
  151. function (sourcePosition, position, tag, html, isClosing) {
  152. var info = {
  153. sourcePosition: sourcePosition,
  154. position: position,
  155. isClosing: isClosing,
  156. isWhite: Object.prototype.hasOwnProperty.call(whiteList, tag),
  157. };
  158. // call `onTag()`
  159. var ret = onTag(tag, html, info);
  160. if (!isNull(ret)) return ret;
  161. if (info.isWhite) {
  162. if (info.isClosing) {
  163. return "</" + tag + ">";
  164. }
  165. var attrs = getAttrs(html);
  166. var whiteAttrList = whiteList[tag];
  167. var attrsHtml = parseAttr(attrs.html, function (name, value) {
  168. // call `onTagAttr()`
  169. var isWhiteAttr = _.indexOf(whiteAttrList, name) !== -1;
  170. var ret = onTagAttr(tag, name, value, isWhiteAttr);
  171. if (!isNull(ret)) return ret;
  172. if (isWhiteAttr) {
  173. // call `safeAttrValue()`
  174. value = safeAttrValue(tag, name, value, cssFilter);
  175. if (value) {
  176. return name + '=' + attributeWrapSign + value + attributeWrapSign;
  177. } else {
  178. return name;
  179. }
  180. } else {
  181. // call `onIgnoreTagAttr()`
  182. ret = onIgnoreTagAttr(tag, name, value, isWhiteAttr);
  183. if (!isNull(ret)) return ret;
  184. return;
  185. }
  186. });
  187. // build new tag html
  188. html = "<" + tag;
  189. if (attrsHtml) html += " " + attrsHtml;
  190. if (attrs.closing) html += " /";
  191. html += ">";
  192. return html;
  193. } else {
  194. // call `onIgnoreTag()`
  195. ret = onIgnoreTag(tag, html, info);
  196. if (!isNull(ret)) return ret;
  197. return escapeHtml(html);
  198. }
  199. },
  200. escapeHtml
  201. );
  202. // if enable stripIgnoreTagBody
  203. if (stripIgnoreTagBody) {
  204. retHtml = stripIgnoreTagBody.remove(retHtml);
  205. }
  206. return retHtml;
  207. };
  208. module.exports = FilterXSS;