@deepmetier/xss 0.0.0-stage → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/README.md +23 -2
- package/README.zh.md +24 -0
- package/bin/xss +67 -0
- package/dist/xss.js +1784 -0
- package/dist/xss.min.js +1 -0
- package/lib/cli.js +45 -0
- package/lib/default.js +468 -0
- package/lib/index.js +118 -0
- package/lib/parser.js +257 -0
- package/lib/util.js +34 -0
- package/lib/xss.js +237 -0
- package/package.json +63 -4
- package/typings/xss.d.ts +219 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xss
|
|
3
|
+
*
|
|
4
|
+
* @author Zongmin Lei<leizongmin@gmail.com>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
var DEFAULT = require("./default");
|
|
8
|
+
var parser = require("./parser");
|
|
9
|
+
var FilterXSS = require("./xss");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* filter xss function
|
|
13
|
+
*
|
|
14
|
+
* @param {String} html
|
|
15
|
+
* @param {Object} options { whiteList, onTag, onTagAttr, onIgnoreTag, onIgnoreTagAttr, safeAttrValue, escapeHtml }
|
|
16
|
+
* @return {String}
|
|
17
|
+
*/
|
|
18
|
+
function filterXSS(html, options) {
|
|
19
|
+
var xss = new FilterXSS(options);
|
|
20
|
+
return xss.process(html);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Filter XSS and return both the sanitized HTML and a list of removed elements.
|
|
25
|
+
* Addresses feature request: https://github.com/leizongmin/js-xss/issues/284
|
|
26
|
+
*
|
|
27
|
+
* @param {String} html - dirty HTML string to sanitize
|
|
28
|
+
* @param {Object} options - same options as filterXSS()
|
|
29
|
+
* @return {Object} { html: String, removed: Array }
|
|
30
|
+
*
|
|
31
|
+
* Each item in `removed` is one of:
|
|
32
|
+
* { type: "tag", tag: "script", html: "<script>", isClosing: false }
|
|
33
|
+
* { type: "attr", tag: "a", attr: "onclick", value: "evil()" }
|
|
34
|
+
*/
|
|
35
|
+
function filterXSSWithResult(html, options) {
|
|
36
|
+
var removed = [];
|
|
37
|
+
|
|
38
|
+
// Copy options so we don't mutate the caller's object
|
|
39
|
+
var opts = {};
|
|
40
|
+
if (options) {
|
|
41
|
+
for (var key in options) {
|
|
42
|
+
opts[key] = options[key];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Save any hooks the user may have already provided
|
|
47
|
+
var originalOnIgnoreTag = opts.onIgnoreTag;
|
|
48
|
+
var originalOnIgnoreTagAttr = opts.onIgnoreTagAttr;
|
|
49
|
+
|
|
50
|
+
// Override onIgnoreTag to record removed tags
|
|
51
|
+
opts.onIgnoreTag = function (tag, tagHtml, tagOptions) {
|
|
52
|
+
removed.push({
|
|
53
|
+
type: "tag",
|
|
54
|
+
tag: tag,
|
|
55
|
+
html: tagHtml,
|
|
56
|
+
isClosing: !!(tagOptions && tagOptions.isClosing),
|
|
57
|
+
});
|
|
58
|
+
// Still call the user's original hook if they had one
|
|
59
|
+
if (originalOnIgnoreTag) {
|
|
60
|
+
return originalOnIgnoreTag(tag, tagHtml, tagOptions);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Override onIgnoreTagAttr to record removed attributes
|
|
65
|
+
opts.onIgnoreTagAttr = function (tag, name, value, isWhiteAttr) {
|
|
66
|
+
removed.push({
|
|
67
|
+
type: "attr",
|
|
68
|
+
tag: tag,
|
|
69
|
+
attr: name,
|
|
70
|
+
value: value,
|
|
71
|
+
});
|
|
72
|
+
// Still call the user's original hook if they had one
|
|
73
|
+
if (originalOnIgnoreTagAttr) {
|
|
74
|
+
return originalOnIgnoreTagAttr(tag, name, value, isWhiteAttr);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
var cleanHtml = filterXSS(html, opts);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
html: cleanHtml,
|
|
82
|
+
removed: removed,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
exports = module.exports = filterXSS;
|
|
86
|
+
exports.filterXSS = filterXSS;
|
|
87
|
+
exports.filterXSSWithResult = filterXSSWithResult;
|
|
88
|
+
exports.FilterXSS = FilterXSS;
|
|
89
|
+
|
|
90
|
+
(function () {
|
|
91
|
+
for (var i in DEFAULT) {
|
|
92
|
+
if (Object.prototype.hasOwnProperty.call(DEFAULT, i)) {
|
|
93
|
+
exports[i] = DEFAULT[i];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (var j in parser) {
|
|
97
|
+
if (Object.prototype.hasOwnProperty.call(parser, j)) {
|
|
98
|
+
exports[j] = parser[j];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
|
|
103
|
+
// using `xss` on the browser, output `filterXSS` to the globals
|
|
104
|
+
if (typeof window !== "undefined") {
|
|
105
|
+
window.filterXSS = module.exports;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// using `xss` on the WebWorker, output `filterXSS` to the globals
|
|
109
|
+
function isWorkerEnv() {
|
|
110
|
+
return (
|
|
111
|
+
typeof self !== "undefined" &&
|
|
112
|
+
typeof DedicatedWorkerGlobalScope !== "undefined" &&
|
|
113
|
+
self instanceof DedicatedWorkerGlobalScope
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (isWorkerEnv()) {
|
|
117
|
+
self.filterXSS = module.exports;
|
|
118
|
+
}
|
package/lib/parser.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple HTML Parser
|
|
3
|
+
*
|
|
4
|
+
* @author Zongmin Lei<leizongmin@gmail.com>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
var _ = require("./util");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* get tag name
|
|
11
|
+
*
|
|
12
|
+
* @param {String} html e.g. '<a hef="#">'
|
|
13
|
+
* @return {String}
|
|
14
|
+
*/
|
|
15
|
+
function getTagName(html) {
|
|
16
|
+
var i = _.spaceIndex(html);
|
|
17
|
+
var tagName;
|
|
18
|
+
if (i === -1) {
|
|
19
|
+
tagName = html.slice(1, -1);
|
|
20
|
+
} else {
|
|
21
|
+
tagName = html.slice(1, i + 1);
|
|
22
|
+
}
|
|
23
|
+
tagName = _.trim(tagName).toLowerCase();
|
|
24
|
+
if (tagName.slice(0, 1) === "/") tagName = tagName.slice(1);
|
|
25
|
+
if (tagName.slice(-1) === "/") tagName = tagName.slice(0, -1);
|
|
26
|
+
return tagName;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* is close tag?
|
|
31
|
+
*
|
|
32
|
+
* @param {String} html 如:'<a hef="#">'
|
|
33
|
+
* @return {Boolean}
|
|
34
|
+
*/
|
|
35
|
+
function isClosing(html) {
|
|
36
|
+
return html.slice(0, 2) === "</";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* parse input html and returns processed html
|
|
41
|
+
*
|
|
42
|
+
* @param {String} html
|
|
43
|
+
* @param {Function} onTag e.g. function (sourcePosition, position, tag, html, isClosing)
|
|
44
|
+
* @param {Function} escapeHtml
|
|
45
|
+
* @return {String}
|
|
46
|
+
*/
|
|
47
|
+
function parseTag(html, onTag, escapeHtml) {
|
|
48
|
+
"use strict";
|
|
49
|
+
|
|
50
|
+
var rethtml = "";
|
|
51
|
+
var lastPos = 0;
|
|
52
|
+
var tagStart = false;
|
|
53
|
+
var quoteStart = false;
|
|
54
|
+
var currentPos = 0;
|
|
55
|
+
var len = html.length;
|
|
56
|
+
var currentTagName = "";
|
|
57
|
+
var currentHtml = "";
|
|
58
|
+
|
|
59
|
+
chariterator: for (currentPos = 0; currentPos < len; currentPos++) {
|
|
60
|
+
var c = html.charAt(currentPos);
|
|
61
|
+
if (tagStart === false) {
|
|
62
|
+
if (c === "<") {
|
|
63
|
+
tagStart = currentPos;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
if (quoteStart === false) {
|
|
68
|
+
if (c === "<") {
|
|
69
|
+
rethtml += escapeHtml(html.slice(lastPos, currentPos));
|
|
70
|
+
tagStart = currentPos;
|
|
71
|
+
lastPos = currentPos;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (c === ">" || currentPos === len - 1) {
|
|
75
|
+
rethtml += escapeHtml(html.slice(lastPos, tagStart));
|
|
76
|
+
currentHtml = html.slice(tagStart, currentPos + 1);
|
|
77
|
+
currentTagName = getTagName(currentHtml);
|
|
78
|
+
rethtml += onTag(
|
|
79
|
+
tagStart,
|
|
80
|
+
rethtml.length,
|
|
81
|
+
currentTagName,
|
|
82
|
+
currentHtml,
|
|
83
|
+
isClosing(currentHtml)
|
|
84
|
+
);
|
|
85
|
+
lastPos = currentPos + 1;
|
|
86
|
+
tagStart = false;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (c === '"' || c === "'") {
|
|
90
|
+
var i = 1;
|
|
91
|
+
var ic = html.charAt(currentPos - i);
|
|
92
|
+
|
|
93
|
+
while (ic.trim() === "" || ic === "=") {
|
|
94
|
+
if (ic === "=") {
|
|
95
|
+
quoteStart = c;
|
|
96
|
+
continue chariterator;
|
|
97
|
+
}
|
|
98
|
+
ic = html.charAt(currentPos - ++i);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
if (c === quoteStart) {
|
|
103
|
+
quoteStart = false;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (lastPos < len) {
|
|
110
|
+
rethtml += escapeHtml(html.substr(lastPos));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return rethtml;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
var REGEXP_ILLEGAL_ATTR_NAME = /[^a-zA-Z0-9\\_:.-]/gim;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* parse input attributes and returns processed attributes
|
|
120
|
+
*
|
|
121
|
+
* @param {String} html e.g. `href="#" target="_blank"`
|
|
122
|
+
* @param {Function} onAttr e.g. `function (name, value)`
|
|
123
|
+
* @return {String}
|
|
124
|
+
*/
|
|
125
|
+
function parseAttr(html, onAttr) {
|
|
126
|
+
"use strict";
|
|
127
|
+
|
|
128
|
+
var lastPos = 0;
|
|
129
|
+
var lastMarkPos = 0;
|
|
130
|
+
var retAttrs = [];
|
|
131
|
+
var tmpName = false;
|
|
132
|
+
var len = html.length;
|
|
133
|
+
|
|
134
|
+
function addAttr(name, value) {
|
|
135
|
+
name = _.trim(name);
|
|
136
|
+
name = name.replace(REGEXP_ILLEGAL_ATTR_NAME, "").toLowerCase();
|
|
137
|
+
if (name.length < 1) return;
|
|
138
|
+
var ret = onAttr(name, value || "");
|
|
139
|
+
if (ret) retAttrs.push(ret);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 逐个分析字符
|
|
143
|
+
for (var i = 0; i < len; i++) {
|
|
144
|
+
var c = html.charAt(i);
|
|
145
|
+
var v, j;
|
|
146
|
+
if (tmpName === false && c === "=") {
|
|
147
|
+
tmpName = html.slice(lastPos, i);
|
|
148
|
+
lastPos = i + 1;
|
|
149
|
+
lastMarkPos = html.charAt(lastPos) === '"' || html.charAt(lastPos) === "'" ? lastPos : findNextQuotationMark(html, i + 1);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (tmpName !== false) {
|
|
153
|
+
if (
|
|
154
|
+
i === lastMarkPos
|
|
155
|
+
) {
|
|
156
|
+
j = html.indexOf(c, i + 1);
|
|
157
|
+
if (j === -1) {
|
|
158
|
+
break;
|
|
159
|
+
} else {
|
|
160
|
+
v = _.trim(html.slice(lastMarkPos + 1, j));
|
|
161
|
+
addAttr(tmpName, v);
|
|
162
|
+
tmpName = false;
|
|
163
|
+
i = j;
|
|
164
|
+
lastPos = i + 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (/\s|\n|\t/.test(c)) {
|
|
170
|
+
html = html.replace(/\s|\n|\t/g, " ");
|
|
171
|
+
if (tmpName === false) {
|
|
172
|
+
j = findNextEqual(html, i);
|
|
173
|
+
if (j === -1) {
|
|
174
|
+
v = _.trim(html.slice(lastPos, i));
|
|
175
|
+
addAttr(v);
|
|
176
|
+
tmpName = false;
|
|
177
|
+
lastPos = i + 1;
|
|
178
|
+
continue;
|
|
179
|
+
} else {
|
|
180
|
+
i = j - 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
j = findBeforeEqual(html, i - 1);
|
|
185
|
+
if (j === -1) {
|
|
186
|
+
v = _.trim(html.slice(lastPos, i));
|
|
187
|
+
v = stripQuoteWrap(v);
|
|
188
|
+
addAttr(tmpName, v);
|
|
189
|
+
tmpName = false;
|
|
190
|
+
lastPos = i + 1;
|
|
191
|
+
continue;
|
|
192
|
+
} else {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (lastPos < html.length) {
|
|
200
|
+
if (tmpName === false) {
|
|
201
|
+
addAttr(html.slice(lastPos));
|
|
202
|
+
} else {
|
|
203
|
+
addAttr(tmpName, stripQuoteWrap(_.trim(html.slice(lastPos))));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return _.trim(retAttrs.join(" "));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function findNextEqual(str, i) {
|
|
211
|
+
for (; i < str.length; i++) {
|
|
212
|
+
var c = str[i];
|
|
213
|
+
if (c === " ") continue;
|
|
214
|
+
if (c === "=") return i;
|
|
215
|
+
return -1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function findNextQuotationMark(str, i) {
|
|
220
|
+
for (; i < str.length; i++) {
|
|
221
|
+
var c = str[i];
|
|
222
|
+
if (c === " ") continue;
|
|
223
|
+
if (c === "'" || c === '"') return i;
|
|
224
|
+
return -1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function findBeforeEqual(str, i) {
|
|
229
|
+
for (; i > 0; i--) {
|
|
230
|
+
var c = str[i];
|
|
231
|
+
if (c === " ") continue;
|
|
232
|
+
if (c === "=") return i;
|
|
233
|
+
return -1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isQuoteWrapString(text) {
|
|
238
|
+
if (
|
|
239
|
+
(text[0] === '"' && text[text.length - 1] === '"') ||
|
|
240
|
+
(text[0] === "'" && text[text.length - 1] === "'")
|
|
241
|
+
) {
|
|
242
|
+
return true;
|
|
243
|
+
} else {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function stripQuoteWrap(text) {
|
|
249
|
+
if (isQuoteWrapString(text)) {
|
|
250
|
+
return text.substr(1, text.length - 2);
|
|
251
|
+
} else {
|
|
252
|
+
return text;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
exports.parseTag = parseTag;
|
|
257
|
+
exports.parseAttr = parseAttr;
|
package/lib/util.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
indexOf: function (arr, item) {
|
|
3
|
+
var i, j;
|
|
4
|
+
if (Array.prototype.indexOf) {
|
|
5
|
+
return arr.indexOf(item);
|
|
6
|
+
}
|
|
7
|
+
for (i = 0, j = arr.length; i < j; i++) {
|
|
8
|
+
if (arr[i] === item) {
|
|
9
|
+
return i;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return -1;
|
|
13
|
+
},
|
|
14
|
+
forEach: function (arr, fn, scope) {
|
|
15
|
+
var i, j;
|
|
16
|
+
if (Array.prototype.forEach) {
|
|
17
|
+
return arr.forEach(fn, scope);
|
|
18
|
+
}
|
|
19
|
+
for (i = 0, j = arr.length; i < j; i++) {
|
|
20
|
+
fn.call(scope, arr[i], i, arr);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
trim: function (str) {
|
|
24
|
+
if (String.prototype.trim) {
|
|
25
|
+
return str.trim();
|
|
26
|
+
}
|
|
27
|
+
return str.replace(/(^\s*)|(\s*$)/g, "");
|
|
28
|
+
},
|
|
29
|
+
spaceIndex: function (str) {
|
|
30
|
+
var reg = /\s|\n|\t/;
|
|
31
|
+
var match = reg.exec(str);
|
|
32
|
+
return match ? match.index : -1;
|
|
33
|
+
},
|
|
34
|
+
};
|
package/lib/xss.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* filter xss
|
|
3
|
+
*
|
|
4
|
+
* @author Zongmin Lei<leizongmin@gmail.com>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
var FilterCSS = require("@deepmetier/cssfilter").FilterCSS;
|
|
8
|
+
var DEFAULT = require("./default");
|
|
9
|
+
var parser = require("./parser");
|
|
10
|
+
var parseTag = parser.parseTag;
|
|
11
|
+
var parseAttr = parser.parseAttr;
|
|
12
|
+
var _ = require("./util");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* returns `true` if the input value is `undefined` or `null`
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} obj
|
|
18
|
+
* @return {Boolean}
|
|
19
|
+
*/
|
|
20
|
+
function 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
|
+
*/
|
|
32
|
+
function 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
|
+
*/
|
|
55
|
+
function shallowCopyObject(obj) {
|
|
56
|
+
var ret = {};
|
|
57
|
+
for (var i in obj) {
|
|
58
|
+
if (Object.prototype.hasOwnProperty.call(obj, i)) {
|
|
59
|
+
ret[i] = obj[i];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return ret;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function keysToLowerCase(obj) {
|
|
66
|
+
var ret = {};
|
|
67
|
+
for (var i in obj) {
|
|
68
|
+
if (Object.prototype.hasOwnProperty.call(obj, i)) {
|
|
69
|
+
if (Array.isArray(obj[i])) {
|
|
70
|
+
ret[i.toLowerCase()] = obj[i].map(function (item) {
|
|
71
|
+
return item.toLowerCase();
|
|
72
|
+
});
|
|
73
|
+
} else {
|
|
74
|
+
ret[i.toLowerCase()] = obj[i];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return ret;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* FilterXSS class
|
|
83
|
+
*
|
|
84
|
+
* @param {Object} options
|
|
85
|
+
* whiteList (or allowList), onTag, onTagAttr, onIgnoreTag,
|
|
86
|
+
* onIgnoreTagAttr, safeAttrValue, escapeHtml
|
|
87
|
+
* stripIgnoreTagBody, allowCommentTag, stripBlankChar
|
|
88
|
+
* css{whiteList, onAttr, onIgnoreAttr} `css=false` means don't use `cssfilter`
|
|
89
|
+
*/
|
|
90
|
+
function FilterXSS(options) {
|
|
91
|
+
options = shallowCopyObject(options || {});
|
|
92
|
+
|
|
93
|
+
if (Object.prototype.hasOwnProperty.call(options, 'stripIgnoreTag') && options.stripIgnoreTag) {
|
|
94
|
+
if (Object.prototype.hasOwnProperty.call(options, 'onIgnoreTag') && options.onIgnoreTag) {
|
|
95
|
+
console.error(
|
|
96
|
+
'Notes: cannot use these two options "stripIgnoreTag" and "onIgnoreTag" at the same time'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
options.onIgnoreTag = DEFAULT.onIgnoreTagStripAll;
|
|
100
|
+
}
|
|
101
|
+
if (Object.prototype.hasOwnProperty.call(options, 'whiteList') || Object.prototype.hasOwnProperty.call(options, 'allowList')) {
|
|
102
|
+
const wl = Object.prototype.hasOwnProperty.call(options, 'whiteList') ? options.whiteList : options.allowList;
|
|
103
|
+
options.whiteList = keysToLowerCase(wl);
|
|
104
|
+
} else {
|
|
105
|
+
options.whiteList = DEFAULT.whiteList;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.attributeWrapSign = Object.prototype.hasOwnProperty.call(options, 'singleQuotedAttributeValue') && options.singleQuotedAttributeValue === true ? "'" : DEFAULT.attributeWrapSign;
|
|
109
|
+
|
|
110
|
+
options.onTag = Object.prototype.hasOwnProperty.call(options, 'onTag') ? options.onTag : DEFAULT.onTag;
|
|
111
|
+
options.onTagAttr = Object.prototype.hasOwnProperty.call(options, 'onTagAttr') ? options.onTagAttr : DEFAULT.onTagAttr;
|
|
112
|
+
options.onIgnoreTag = Object.prototype.hasOwnProperty.call(options, 'onIgnoreTag') ? options.onIgnoreTag : DEFAULT.onIgnoreTag;
|
|
113
|
+
options.onIgnoreTagAttr = Object.prototype.hasOwnProperty.call(options, 'onIgnoreTagAttr') ? options.onIgnoreTagAttr : DEFAULT.onIgnoreTagAttr;
|
|
114
|
+
options.safeAttrValue = Object.prototype.hasOwnProperty.call(options, 'safeAttrValue') ? options.safeAttrValue : DEFAULT.safeAttrValue;
|
|
115
|
+
options.escapeHtml = Object.prototype.hasOwnProperty.call(options, 'escapeHtml') ? options.escapeHtml : DEFAULT.escapeHtml;
|
|
116
|
+
this.options = options;
|
|
117
|
+
|
|
118
|
+
if (Object.prototype.hasOwnProperty.call(options, 'css') && options.css === false) {
|
|
119
|
+
this.cssFilter = false;
|
|
120
|
+
} else {
|
|
121
|
+
options.css = Object.prototype.hasOwnProperty.call(options, 'css') ? options.css : {};
|
|
122
|
+
this.cssFilter = new FilterCSS(options.css);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* start process and returns result
|
|
128
|
+
*
|
|
129
|
+
* @param {String} html
|
|
130
|
+
* @return {String}
|
|
131
|
+
*/
|
|
132
|
+
FilterXSS.prototype.process = function (html) {
|
|
133
|
+
// compatible with the input
|
|
134
|
+
html = html || "";
|
|
135
|
+
html = html.toString();
|
|
136
|
+
if (!html) return "";
|
|
137
|
+
|
|
138
|
+
var me = this;
|
|
139
|
+
var options = me.options;
|
|
140
|
+
var whiteList = options.whiteList;
|
|
141
|
+
var onTag = options.onTag;
|
|
142
|
+
var onIgnoreTag = options.onIgnoreTag;
|
|
143
|
+
var onTagAttr = options.onTagAttr;
|
|
144
|
+
var onIgnoreTagAttr = options.onIgnoreTagAttr;
|
|
145
|
+
var safeAttrValue = options.safeAttrValue;
|
|
146
|
+
var escapeHtml = options.escapeHtml;
|
|
147
|
+
var attributeWrapSign = me.attributeWrapSign;
|
|
148
|
+
var cssFilter = me.cssFilter;
|
|
149
|
+
|
|
150
|
+
// remove invisible characters
|
|
151
|
+
if (options.stripBlankChar) {
|
|
152
|
+
html = DEFAULT.stripBlankChar(html);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// remove html comments
|
|
156
|
+
if (!options.allowCommentTag) {
|
|
157
|
+
html = DEFAULT.stripCommentTag(html);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// if enable stripIgnoreTagBody
|
|
161
|
+
var stripIgnoreTagBody = false;
|
|
162
|
+
if (options.stripIgnoreTagBody) {
|
|
163
|
+
stripIgnoreTagBody = DEFAULT.StripTagBody(
|
|
164
|
+
options.stripIgnoreTagBody,
|
|
165
|
+
onIgnoreTag
|
|
166
|
+
);
|
|
167
|
+
onIgnoreTag = stripIgnoreTagBody.onIgnoreTag;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
var retHtml = parseTag(
|
|
171
|
+
html,
|
|
172
|
+
function (sourcePosition, position, tag, html, isClosing) {
|
|
173
|
+
var info = {
|
|
174
|
+
sourcePosition: sourcePosition,
|
|
175
|
+
position: position,
|
|
176
|
+
isClosing: isClosing,
|
|
177
|
+
isWhite: Object.prototype.hasOwnProperty.call(whiteList, tag),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// call `onTag()`
|
|
181
|
+
var ret = onTag(tag, html, info);
|
|
182
|
+
if (!isNull(ret)) return ret;
|
|
183
|
+
|
|
184
|
+
if (info.isWhite) {
|
|
185
|
+
if (info.isClosing) {
|
|
186
|
+
return "</" + tag + ">";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
var attrs = getAttrs(html);
|
|
190
|
+
var whiteAttrList = whiteList[tag];
|
|
191
|
+
var attrsHtml = parseAttr(attrs.html, function (name, value) {
|
|
192
|
+
// call `onTagAttr()`
|
|
193
|
+
var isWhiteAttr = _.indexOf(whiteAttrList, name) !== -1;
|
|
194
|
+
var ret = onTagAttr(tag, name, value, isWhiteAttr);
|
|
195
|
+
if (!isNull(ret)) return ret;
|
|
196
|
+
|
|
197
|
+
if (isWhiteAttr) {
|
|
198
|
+
// call `safeAttrValue()`
|
|
199
|
+
value = safeAttrValue(tag, name, value, cssFilter);
|
|
200
|
+
if (value) {
|
|
201
|
+
return name + '=' + attributeWrapSign + value + attributeWrapSign;
|
|
202
|
+
} else {
|
|
203
|
+
return name;
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
// call `onIgnoreTagAttr()`
|
|
207
|
+
ret = onIgnoreTagAttr(tag, name, value, isWhiteAttr);
|
|
208
|
+
if (!isNull(ret)) return ret;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// build new tag html
|
|
214
|
+
html = "<" + tag;
|
|
215
|
+
if (attrsHtml) html += " " + attrsHtml;
|
|
216
|
+
if (attrs.closing) html += " /";
|
|
217
|
+
html += ">";
|
|
218
|
+
return html;
|
|
219
|
+
} else {
|
|
220
|
+
// call `onIgnoreTag()`
|
|
221
|
+
ret = onIgnoreTag(tag, html, info);
|
|
222
|
+
if (!isNull(ret)) return ret;
|
|
223
|
+
return escapeHtml(html);
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
escapeHtml
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// if enable stripIgnoreTagBody
|
|
230
|
+
if (stripIgnoreTagBody) {
|
|
231
|
+
retHtml = stripIgnoreTagBody.remove(retHtml);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return retHtml;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
module.exports = FilterXSS;
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepmetier/xss",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"main": "./lib/index.js",
|
|
4
|
+
"typings": "./typings/xss.d.ts",
|
|
5
|
+
"version": "1.0.16",
|
|
6
|
+
"description": "Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist",
|
|
7
|
+
"author": "Zongmin Lei <leizongmin@gmail.com> (http://ucdok.com)",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git://github.com/leizongmin/js-xss.git"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">= 0.10.0"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@deepmetier/commander": "^2.20.3",
|
|
17
|
+
"@deepmetier/cssfilter": "0.0.10"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"browserify": "^17.0.0",
|
|
21
|
+
"coveralls": "^3.1.1",
|
|
22
|
+
"debug": "^4.3.4",
|
|
23
|
+
"eslint": "^8.16.0",
|
|
24
|
+
"mocha": "^8.4.0",
|
|
25
|
+
"nyc": "^15.1.0",
|
|
26
|
+
"uglify-js": "^3.15.5"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib",
|
|
30
|
+
"bin/xss",
|
|
31
|
+
"dist",
|
|
32
|
+
"typings/*.d.ts"
|
|
33
|
+
],
|
|
34
|
+
"bin": {
|
|
35
|
+
"xss": "./bin/xss"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"lint": "eslint lib/**",
|
|
39
|
+
"test": "export DEBUG=xss:* && mocha -t 5000",
|
|
40
|
+
"test-cov": "nyc --reporter=lcov mocha --exit \"test/*.js\" && nyc report",
|
|
41
|
+
"coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
|
|
42
|
+
"build": "./bin/build",
|
|
43
|
+
"prepublish": "npm run test && npm run build"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/leizongmin/js-xss/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/leizongmin/js-xss",
|
|
50
|
+
"keywords": [
|
|
51
|
+
"sanitization",
|
|
52
|
+
"xss",
|
|
53
|
+
"sanitize",
|
|
54
|
+
"sanitisation",
|
|
55
|
+
"input",
|
|
56
|
+
"security",
|
|
57
|
+
"escape",
|
|
58
|
+
"encode",
|
|
59
|
+
"filter",
|
|
60
|
+
"validator",
|
|
61
|
+
"html",
|
|
62
|
+
"injection",
|
|
63
|
+
"whitelist"
|
|
64
|
+
]
|
|
65
|
+
}
|