UNPKG

6.01 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
63function keysToLowerCase(obj) {
64 var ret = {};
65 for (var i in obj) {
66 if (Array.isArray(obj[i])) {
67 ret[i.toLowerCase()] = obj[i].map(function (item) {
68 return item.toLowerCase();
69 });
70 } else {
71 ret[i.toLowerCase()] = obj[i];
72 }
73 }
74 return ret;
75}
76
77/**
78 * FilterXSS class
79 *
80 * @param {Object} options
81 * whiteList (or allowList), onTag, onTagAttr, onIgnoreTag,
82 * onIgnoreTagAttr, safeAttrValue, escapeHtml
83 * stripIgnoreTagBody, allowCommentTag, stripBlankChar
84 * css{whiteList, onAttr, onIgnoreAttr} `css=false` means don't use `cssfilter`
85 */
86function FilterXSS(options) {
87 options = shallowCopyObject(options || {});
88
89 if (options.stripIgnoreTag) {
90 if (options.onIgnoreTag) {
91 console.error(
92 'Notes: cannot use these two options "stripIgnoreTag" and "onIgnoreTag" at the same time'
93 );
94 }
95 options.onIgnoreTag = DEFAULT.onIgnoreTagStripAll;
96 }
97 if (options.whiteList || options.allowList) {
98 options.whiteList = keysToLowerCase(options.whiteList || options.allowList);
99 } else {
100 options.whiteList = DEFAULT.whiteList;
101 }
102
103 this.attributeWrapSign = options.singleQuotedAttributeValue === true ? "'" : DEFAULT.attributeWrapSign;
104
105 options.onTag = options.onTag || DEFAULT.onTag;
106 options.onTagAttr = options.onTagAttr || DEFAULT.onTagAttr;
107 options.onIgnoreTag = options.onIgnoreTag || DEFAULT.onIgnoreTag;
108 options.onIgnoreTagAttr = options.onIgnoreTagAttr || DEFAULT.onIgnoreTagAttr;
109 options.safeAttrValue = options.safeAttrValue || DEFAULT.safeAttrValue;
110 options.escapeHtml = options.escapeHtml || DEFAULT.escapeHtml;
111 this.options = options;
112
113 if (options.css === false) {
114 this.cssFilter = false;
115 } else {
116 options.css = options.css || {};
117 this.cssFilter = new FilterCSS(options.css);
118 }
119}
120
121/**
122 * start process and returns result
123 *
124 * @param {String} html
125 * @return {String}
126 */
127FilterXSS.prototype.process = function (html) {
128 // compatible with the input
129 html = html || "";
130 html = html.toString();
131 if (!html) return "";
132
133 var me = this;
134 var options = me.options;
135 var whiteList = options.whiteList;
136 var onTag = options.onTag;
137 var onIgnoreTag = options.onIgnoreTag;
138 var onTagAttr = options.onTagAttr;
139 var onIgnoreTagAttr = options.onIgnoreTagAttr;
140 var safeAttrValue = options.safeAttrValue;
141 var escapeHtml = options.escapeHtml;
142 var attributeWrapSign = me.attributeWrapSign;
143 var cssFilter = me.cssFilter;
144
145 // remove invisible characters
146 if (options.stripBlankChar) {
147 html = DEFAULT.stripBlankChar(html);
148 }
149
150 // remove html comments
151 if (!options.allowCommentTag) {
152 html = DEFAULT.stripCommentTag(html);
153 }
154
155 // if enable stripIgnoreTagBody
156 var stripIgnoreTagBody = false;
157 if (options.stripIgnoreTagBody) {
158 stripIgnoreTagBody = DEFAULT.StripTagBody(
159 options.stripIgnoreTagBody,
160 onIgnoreTag
161 );
162 onIgnoreTag = stripIgnoreTagBody.onIgnoreTag;
163 }
164
165 var retHtml = parseTag(
166 html,
167 function (sourcePosition, position, tag, html, isClosing) {
168 var info = {
169 sourcePosition: sourcePosition,
170 position: position,
171 isClosing: isClosing,
172 isWhite: Object.prototype.hasOwnProperty.call(whiteList, tag),
173 };
174
175 // call `onTag()`
176 var ret = onTag(tag, html, info);
177 if (!isNull(ret)) return ret;
178
179 if (info.isWhite) {
180 if (info.isClosing) {
181 return "</" + tag + ">";
182 }
183
184 var attrs = getAttrs(html);
185 var whiteAttrList = whiteList[tag];
186 var attrsHtml = parseAttr(attrs.html, function (name, value) {
187 // call `onTagAttr()`
188 var isWhiteAttr = _.indexOf(whiteAttrList, name) !== -1;
189 var ret = onTagAttr(tag, name, value, isWhiteAttr);
190 if (!isNull(ret)) return ret;
191
192 if (isWhiteAttr) {
193 // call `safeAttrValue()`
194 value = safeAttrValue(tag, name, value, cssFilter);
195 if (value) {
196 return name + '=' + attributeWrapSign + value + attributeWrapSign;
197 } else {
198 return name;
199 }
200 } else {
201 // call `onIgnoreTagAttr()`
202 ret = onIgnoreTagAttr(tag, name, value, isWhiteAttr);
203 if (!isNull(ret)) return ret;
204 return;
205 }
206 });
207
208 // build new tag html
209 html = "<" + tag;
210 if (attrsHtml) html += " " + attrsHtml;
211 if (attrs.closing) html += " /";
212 html += ">";
213 return html;
214 } else {
215 // call `onIgnoreTag()`
216 ret = onIgnoreTag(tag, html, info);
217 if (!isNull(ret)) return ret;
218 return escapeHtml(html);
219 }
220 },
221 escapeHtml
222 );
223
224 // if enable stripIgnoreTagBody
225 if (stripIgnoreTagBody) {
226 retHtml = stripIgnoreTagBody.remove(retHtml);
227 }
228
229 return retHtml;
230};
231
232module.exports = FilterXSS;