UNPKG

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