• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

leizongmin / js-xss / #149

08 Nov 2023 01:19PM UTC coverage: 95.567%. Remained the same
#149

push

travis-pro

123robi
add support for onTagRemoved

219 of 241 branches covered (0.0%)

5 of 6 new or added lines in 2 files covered. (83.33%)

3 existing lines in 1 file now uncovered.

388 of 406 relevant lines covered (95.57%)

176.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.01
/lib/default.js
1
/**
2
 * default settings
3
 *
4
 * @author Zongmin Lei<leizongmin@gmail.com>
5
 */
6

7
var FilterCSS = require("cssfilter").FilterCSS;
1✔
8
var getDefaultCSSWhiteList = require("cssfilter").getDefaultWhiteList;
1✔
9
var _ = require("./util");
1✔
10

11
function getDefaultWhiteList() {
12
  return {
2✔
13
    a: ["target", "href", "title"],
14
    abbr: ["title"],
15
    address: [],
16
    area: ["shape", "coords", "href", "alt"],
17
    article: [],
18
    aside: [],
19
    audio: [
20
      "autoplay",
21
      "controls",
22
      "crossorigin",
23
      "loop",
24
      "muted",
25
      "preload",
26
      "src",
27
    ],
28
    b: [],
29
    bdi: ["dir"],
30
    bdo: ["dir"],
31
    big: [],
32
    blockquote: ["cite"],
33
    br: [],
34
    caption: [],
35
    center: [],
36
    cite: [],
37
    code: [],
38
    col: ["align", "valign", "span", "width"],
39
    colgroup: ["align", "valign", "span", "width"],
40
    dd: [],
41
    del: ["datetime"],
42
    details: ["open"],
43
    div: [],
44
    dl: [],
45
    dt: [],
46
    em: [],
47
    figcaption: [],
48
    figure: [],
49
    font: ["color", "size", "face"],
50
    footer: [],
51
    h1: [],
52
    h2: [],
53
    h3: [],
54
    h4: [],
55
    h5: [],
56
    h6: [],
57
    header: [],
58
    hr: [],
59
    i: [],
60
    img: ["src", "alt", "title", "width", "height", "loading"],
61
    ins: ["datetime"],
62
    li: [],
63
    mark: [],
64
    nav: [],
65
    ol: [],
66
    p: [],
67
    pre: [],
68
    s: [],
69
    section: [],
70
    small: [],
71
    span: [],
72
    sub: [],
73
    summary: [],
74
    sup: [],
75
    strong: [],
76
    strike: [],
77
    table: ["width", "border", "align", "valign"],
78
    tbody: ["align", "valign"],
79
    td: ["width", "rowspan", "colspan", "align", "valign"],
80
    tfoot: ["align", "valign"],
81
    th: ["width", "rowspan", "colspan", "align", "valign"],
82
    thead: ["align", "valign"],
83
    tr: ["rowspan", "align", "valign"],
84
    tt: [],
85
    u: [],
86
    ul: [],
87
    video: [
88
      "autoplay",
89
      "controls",
90
      "crossorigin",
91
      "loop",
92
      "muted",
93
      "playsinline",
94
      "poster",
95
      "preload",
96
      "src",
97
      "height",
98
      "width",
99
    ],
100
  };
101
}
102

103
var defaultCSSFilter = new FilterCSS();
1✔
104

105
/**
106
 * default onTag function
107
 *
108
 * @param {String} tag
109
 * @param {String} html
110
 * @param {Object} options
111
 * @return {String}
112
 */
113
function onTag(tag, html, options) {
114
  // do nothing
115
}
116

117
/**
118
 * default onIgnoreTag function
119
 *
120
 * @param {String} tag
121
 * @param {String} html
122
 * @param {Object} options
123
 * @return {String}
124
 */
125
function onIgnoreTag(tag, html, options) {
126
  // do nothing
127
}
128

129
/**
130
 * default onTagAttr function
131
 *
132
 * @param {String} tag
133
 * @param {String} name
134
 * @param {String} value
135
 * @return {String}
136
 */
137
function onTagAttr(tag, name, value) {
138
  // do nothing
139
}
140

141
/**
142
 * default onIgnoreTagAttr function
143
 *
144
 * @param {String} tag
145
 * @param {String} name
146
 * @param {String} value
147
 * @return {String}
148
 */
149
function onIgnoreTagAttr(tag, name, value) {
150
  // do nothing
151
}
152

153
/**
154
 * default escapeHtml function
155
 *
156
 * @param {String} html
157
 */
158
function escapeHtml(html) {
159
  return html.replace(REGEXP_LT, "&lt;").replace(REGEXP_GT, "&gt;");
395✔
160
}
161

162
/**
163
 * default safeAttrValue function
164
 *
165
 * @param {String} tag
166
 * @param {String} name
167
 * @param {String} value
168
 * @param {Object} cssFilter
169
 * @return {String}
170
 */
171
function safeAttrValue(tag, name, value, cssFilter) {
172
  // unescape attribute value firstly
173
  value = friendlyAttrValue(value);
118✔
174

175
  if (name === "href" || name === "src") {
118✔
176
    // filter `href` and `src` attribute
177
    // only allow the value that starts with `http://` | `https://` | `mailto:` | `/` | `#`
178
    value = _.trim(value);
51✔
179
    if (value === "#") return "#";
51✔
180
    if (
44✔
181
      !(
182
        value.substr(0, 7) === "http://" ||
404✔
183
        value.substr(0, 8) === "https://" ||
184
        value.substr(0, 7) === "mailto:" ||
185
        value.substr(0, 4) === "tel:" ||
186
        value.substr(0, 11) === "data:image/" ||
187
        value.substr(0, 6) === "ftp://" ||
188
        value.substr(0, 2) === "./" ||
189
        value.substr(0, 3) === "../" ||
190
        value[0] === "#" ||
191
        value[0] === "/"
192
      )
193
    ) {
194
      return "";
36✔
195
    }
196
  } else if (name === "background") {
67✔
197
    // filter `background` attribute (maybe no use)
198
    // `javascript:`
199
    REGEXP_DEFAULT_ON_TAG_ATTR_4.lastIndex = 0;
1✔
200
    if (REGEXP_DEFAULT_ON_TAG_ATTR_4.test(value)) {
1!
201
      return "";
1✔
202
    }
203
  } else if (name === "style") {
66✔
204
    // `expression()`
205
    REGEXP_DEFAULT_ON_TAG_ATTR_7.lastIndex = 0;
8✔
206
    if (REGEXP_DEFAULT_ON_TAG_ATTR_7.test(value)) {
8✔
207
      return "";
1✔
208
    }
209
    // `url()`
210
    REGEXP_DEFAULT_ON_TAG_ATTR_8.lastIndex = 0;
7✔
211
    if (REGEXP_DEFAULT_ON_TAG_ATTR_8.test(value)) {
7✔
212
      REGEXP_DEFAULT_ON_TAG_ATTR_4.lastIndex = 0;
4✔
213
      if (REGEXP_DEFAULT_ON_TAG_ATTR_4.test(value)) {
4✔
214
        return "";
3✔
215
      }
216
    }
217
    if (cssFilter !== false) {
4✔
218
      cssFilter = cssFilter || defaultCSSFilter;
3!
219
      value = cssFilter.process(value);
3✔
220
    }
221
  }
222

223
  // escape `<>"` before returns
224
  value = escapeAttrValue(value);
70✔
225
  return value;
70✔
226
}
227

228
// RegExp list
229
var REGEXP_LT = /</g;
1✔
230
var REGEXP_GT = />/g;
1✔
231
var REGEXP_QUOTE = /"/g;
1✔
232
var REGEXP_QUOTE_2 = /&quot;/g;
1✔
233
var REGEXP_ATTR_VALUE_1 = /&#([a-zA-Z0-9]*);?/gim;
1✔
234
var REGEXP_ATTR_VALUE_COLON = /&colon;?/gim;
1✔
235
var REGEXP_ATTR_VALUE_NEWLINE = /&newline;?/gim;
1✔
236
// var REGEXP_DEFAULT_ON_TAG_ATTR_3 = /\/\*|\*\//gm;
237
var REGEXP_DEFAULT_ON_TAG_ATTR_4 =
238
  /((j\s*a\s*v\s*a|v\s*b|l\s*i\s*v\s*e)\s*s\s*c\s*r\s*i\s*p\s*t\s*|m\s*o\s*c\s*h\s*a):/gi;
1✔
239
// var REGEXP_DEFAULT_ON_TAG_ATTR_5 = /^[\s"'`]*(d\s*a\s*t\s*a\s*)\:/gi;
240
// var REGEXP_DEFAULT_ON_TAG_ATTR_6 = /^[\s"'`]*(d\s*a\s*t\s*a\s*)\:\s*image\//gi;
241
var REGEXP_DEFAULT_ON_TAG_ATTR_7 =
242
  /e\s*x\s*p\s*r\s*e\s*s\s*s\s*i\s*o\s*n\s*\(.*/gi;
1✔
243
var REGEXP_DEFAULT_ON_TAG_ATTR_8 = /u\s*r\s*l\s*\(.*/gi;
1✔
244

245
/**
246
 * escape double quote
247
 *
248
 * @param {String} str
249
 * @return {String} str
250
 */
251
function escapeQuote(str) {
252
  return str.replace(REGEXP_QUOTE, "&quot;");
70✔
253
}
254

255
/**
256
 * unescape double quote
257
 *
258
 * @param {String} str
259
 * @return {String} str
260
 */
261
function unescapeQuote(str) {
262
  return str.replace(REGEXP_QUOTE_2, '"');
118✔
263
}
264

265
/**
266
 * escape html entities
267
 *
268
 * @param {String} str
269
 * @return {String}
270
 */
271
function escapeHtmlEntities(str) {
272
  return str.replace(REGEXP_ATTR_VALUE_1, function replaceUnicode(str, code) {
118✔
273
    return code[0] === "x" || code[0] === "X"
71✔
274
      ? String.fromCharCode(parseInt(code.substr(1), 16))
275
      : String.fromCharCode(parseInt(code, 10));
276
  });
277
}
278

279
/**
280
 * escape html5 new danger entities
281
 *
282
 * @param {String} str
283
 * @return {String}
284
 */
285
function escapeDangerHtml5Entities(str) {
286
  return str
118✔
287
    .replace(REGEXP_ATTR_VALUE_COLON, ":")
288
    .replace(REGEXP_ATTR_VALUE_NEWLINE, " ");
289
}
290

291
/**
292
 * clear nonprintable characters
293
 *
294
 * @param {String} str
295
 * @return {String}
296
 */
297
function clearNonPrintableCharacter(str) {
298
  var str2 = "";
118✔
299
  for (var i = 0, len = str.length; i < len; i++) {
118✔
300
    str2 += str.charCodeAt(i) < 32 ? " " : str.charAt(i);
1,252✔
301
  }
302
  return _.trim(str2);
118✔
303
}
304

305
/**
306
 * get friendly attribute value
307
 *
308
 * @param {String} str
309
 * @return {String}
310
 */
311
function friendlyAttrValue(str) {
312
  str = unescapeQuote(str);
118✔
313
  str = escapeHtmlEntities(str);
118✔
314
  str = escapeDangerHtml5Entities(str);
118✔
315
  str = clearNonPrintableCharacter(str);
118✔
316
  return str;
118✔
317
}
318

319
/**
320
 * unescape attribute value
321
 *
322
 * @param {String} str
323
 * @return {String}
324
 */
325
function escapeAttrValue(str) {
326
  str = escapeQuote(str);
70✔
327
  str = escapeHtml(str);
70✔
328
  return str;
70✔
329
}
330

331
/**
332
 * `onIgnoreTag` function for removing all the tags that are not in whitelist
333
 */
334
function onIgnoreTagStripAll() {
335
  return "";
2✔
336
}
337

338
function onTagRemoved() {
339
  // do nothing by default
340
}
341

342
/**
343
 * remove tag body
344
 * specify a `tags` list, if the tag is not in the `tags` list then process by the specify function (optional)
345
 *
346
 * @param {array} tags
347
 * @param {function} next
348
 */
349
function StripTagBody(tags, next, onTagRemoved) {
350
  if (typeof next !== "function") {
6!
UNCOV
351
    next = function () { };
×
352
  }
353

354
  if (typeof onTagRemoved !== "function") {
6!
NEW
355
    onTagRemoved = function () { };
×
356
  }
357

358
  var isRemoveAllTag = !Array.isArray(tags);
6✔
359
  function isRemoveTag(tag) {
360
    if (isRemoveAllTag) return true;
28✔
361
    return _.indexOf(tags, tag) !== -1;
16✔
362
  }
363

364
  var removeList = [];
6✔
365
  var posStart = false;
6✔
366

367
  return {
6✔
368
    onIgnoreTag: function (tag, html, options) {
369
      if (isRemoveTag(tag)) {
28✔
370
        onTagRemoved(tag)
20✔
371
        if (options.isValidEmpty) {
20!
372
          // Empty tags have no content to remove so just remove them
UNCOV
373
          posStart = false;
×
UNCOV
374
          return '';
×
375
        } else if (options.isClosing) {
20✔
376
          var ret = "";
10✔
377
          var end = options.position + ret.length;
10✔
378
          removeList.push([
10✔
379
            posStart !== false ? posStart : options.position,
10✔
380
            end,
381
          ]);
382
          posStart = false;
10✔
383
          return ret;
10✔
384
        } else {
385
          if (!posStart) {
10✔
386
            posStart = options.position;
8✔
387
          }
388
          return "";
10✔
389
        }
390
      } else {
391
        return next(tag, html, options);
8✔
392
      }
393
    },
394
    remove: function (html) {
395
      var rethtml = "";
6✔
396
      var lastPos = 0;
6✔
397
      _.forEach(removeList, function (pos) {
6✔
398
        rethtml += html.slice(lastPos, pos[0]);
10✔
399
        lastPos = pos[1];
10✔
400
      });
401
      rethtml += html.slice(lastPos);
6✔
402
      return rethtml;
6✔
403
    },
404
  };
405
}
406

407
/**
408
 * remove html comments
409
 *
410
 * @param {String} html
411
 * @return {String}
412
 */
413
function stripCommentTag(html) {
414
  var retHtml = "";
152✔
415
  var lastPos = 0;
152✔
416
  while (lastPos < html.length) {
152✔
417
    var i = html.indexOf("<!--", lastPos);
157✔
418
    if (i === -1) {
157✔
419
      retHtml += html.slice(lastPos);
147✔
420
      break;
147✔
421
    }
422
    retHtml += html.slice(lastPos, i);
10✔
423
    var j = html.indexOf("-->", i);
10✔
424
    if (j === -1) {
10✔
425
      break;
1✔
426
    }
427
    lastPos = j + 3;
9✔
428
  }
429
  return retHtml;
152✔
430
}
431

432
/**
433
 * remove invisible characters
434
 *
435
 * @param {String} html
436
 * @return {String}
437
 */
438
function stripBlankChar(html) {
439
  var chars = html.split("");
1✔
440
  chars = chars.filter(function (char) {
1✔
441
    var c = char.charCodeAt(0);
9✔
442
    if (c === 127) return false;
9!
443
    if (c <= 31) {
9✔
444
      if (c === 10 || c === 13) return true;
6✔
445
      return false;
4✔
446
    }
447
    return true;
3✔
448
  });
449
  return chars.join("");
1✔
450
}
451

452
exports.whiteList = getDefaultWhiteList();
1✔
453
exports.getDefaultWhiteList = getDefaultWhiteList;
1✔
454
exports.onTag = onTag;
1✔
455
exports.onIgnoreTag = onIgnoreTag;
1✔
456
exports.onTagRemoved = onTagRemoved;
1✔
457
exports.onTagAttr = onTagAttr;
1✔
458
exports.onIgnoreTagAttr = onIgnoreTagAttr;
1✔
459
exports.safeAttrValue = safeAttrValue;
1✔
460
exports.escapeHtml = escapeHtml;
1✔
461
exports.escapeQuote = escapeQuote;
1✔
462
exports.unescapeQuote = unescapeQuote;
1✔
463
exports.escapeHtmlEntities = escapeHtmlEntities;
1✔
464
exports.escapeDangerHtml5Entities = escapeDangerHtml5Entities;
1✔
465
exports.clearNonPrintableCharacter = clearNonPrintableCharacter;
1✔
466
exports.friendlyAttrValue = friendlyAttrValue;
1✔
467
exports.escapeAttrValue = escapeAttrValue;
1✔
468
exports.onIgnoreTagStripAll = onIgnoreTagStripAll;
1✔
469
exports.StripTagBody = StripTagBody;
1✔
470
exports.stripCommentTag = stripCommentTag;
1✔
471
exports.stripBlankChar = stripBlankChar;
1✔
472
exports.cssFilter = defaultCSSFilter;
1✔
473
exports.getDefaultCSSWhiteList = getDefaultCSSWhiteList;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc