sanitize-html 2.17.0 → 2.17.2

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.
Files changed (2) hide show
  1. package/index.js +89 -39
  2. package/package.json +15 -14
package/index.js CHANGED
@@ -88,7 +88,8 @@ function sanitizeHtml(html, options, _recursing) {
88
88
  }
89
89
 
90
90
  let result = '';
91
- // Used for hot swapping the result variable with an empty string in order to "capture" the text written to it.
91
+ // Used for hot swapping the result variable with an empty string
92
+ // in order to "capture" the text written to it.
92
93
  let tempResult = '';
93
94
 
94
95
  function Frame(tag, attribs) {
@@ -119,7 +120,8 @@ function sanitizeHtml(html, options, _recursing) {
119
120
  options.parser = Object.assign({}, htmlParserDefaults, options.parser);
120
121
 
121
122
  const tagAllowed = function (name) {
122
- return options.allowedTags === false || (options.allowedTags || []).indexOf(name) > -1;
123
+ return options.allowedTags === false ||
124
+ (options.allowedTags || []).indexOf(name) > -1;
123
125
  };
124
126
 
125
127
  // vulnerableTags
@@ -301,7 +303,8 @@ function sanitizeHtml(html, options, _recursing) {
301
303
  }
302
304
 
303
305
  const isBeingEscaped = skip && (options.disallowedTagsMode === 'escape' || options.disallowedTagsMode === 'recursiveEscape');
304
- const shouldPreserveEscapedAttributes = isBeingEscaped && options.preserveEscapedAttributes;
306
+ const shouldPreserveEscapedAttributes = isBeingEscaped &&
307
+ options.preserveEscapedAttributes;
305
308
 
306
309
  if (shouldPreserveEscapedAttributes) {
307
310
  each(attribs, function(value, a) {
@@ -315,8 +318,10 @@ function sanitizeHtml(html, options, _recursing) {
315
318
  delete frame.attribs[a];
316
319
  return;
317
320
  }
318
- // If the value is empty, check if the attribute is in the allowedEmptyAttributes array.
319
- // If it is not in the allowedEmptyAttributes array, and it is a known non-boolean attribute, delete it
321
+ // If the value is empty, check if the attribute is
322
+ // in the allowedEmptyAttributes array.
323
+ // If it is not in the allowedEmptyAttributes array,
324
+ // and it is a known non-boolean attribute, delete it
320
325
  // List taken from https://html.spec.whatwg.org/multipage/indices.html#attributes-3
321
326
  if (value === '' && (!options.allowedEmptyAttributes.includes(a)) &&
322
327
  (options.nonBooleanAttributes.includes(a) || options.nonBooleanAttributes.includes('*'))) {
@@ -327,9 +332,11 @@ function sanitizeHtml(html, options, _recursing) {
327
332
  // as necessary if there are specific values defined.
328
333
  let passedAllowedAttributesMapCheck = false;
329
334
  if (!allowedAttributesMap ||
330
- (has(allowedAttributesMap, name) && allowedAttributesMap[name].indexOf(a) !== -1) ||
335
+ (has(allowedAttributesMap, name) &&
336
+ allowedAttributesMap[name].indexOf(a) !== -1) ||
331
337
  (allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1) ||
332
- (has(allowedAttributesGlobMap, name) && allowedAttributesGlobMap[name].test(a)) ||
338
+ (has(allowedAttributesGlobMap, name) &&
339
+ allowedAttributesGlobMap[name].test(a)) ||
333
340
  (allowedAttributesGlobMap['*'] && allowedAttributesGlobMap['*'].test(a))) {
334
341
  passedAllowedAttributesMapCheck = true;
335
342
  } else if (allowedAttributesMap && allowedAttributesMap[name]) {
@@ -373,12 +380,14 @@ function sanitizeHtml(html, options, _recursing) {
373
380
  const parsed = parseUrl(value);
374
381
 
375
382
  if (options.allowedScriptHostnames || options.allowedScriptDomains) {
376
- const allowedHostname = (options.allowedScriptHostnames || []).find(function (hostname) {
377
- return hostname === parsed.url.hostname;
378
- });
379
- const allowedDomain = (options.allowedScriptDomains || []).find(function(domain) {
380
- return parsed.url.hostname === domain || parsed.url.hostname.endsWith(`.${domain}`);
381
- });
383
+ const allowedHostname = (options.allowedScriptHostnames || [])
384
+ .find(function (hostname) {
385
+ return hostname === parsed.url.hostname;
386
+ });
387
+ const allowedDomain = (options.allowedScriptDomains || [])
388
+ .find(function(domain) {
389
+ return parsed.url.hostname === domain || parsed.url.hostname.endsWith(`.${domain}`);
390
+ });
382
391
  allowed = allowedHostname || allowedDomain;
383
392
  }
384
393
  } catch (e) {
@@ -402,13 +411,18 @@ function sanitizeHtml(html, options, _recursing) {
402
411
  allowed = has(options, 'allowIframeRelativeUrls')
403
412
  ? options.allowIframeRelativeUrls
404
413
  : (!options.allowedIframeHostnames && !options.allowedIframeDomains);
405
- } else if (options.allowedIframeHostnames || options.allowedIframeDomains) {
406
- const allowedHostname = (options.allowedIframeHostnames || []).find(function (hostname) {
407
- return hostname === parsed.url.hostname;
408
- });
409
- const allowedDomain = (options.allowedIframeDomains || []).find(function(domain) {
410
- return parsed.url.hostname === domain || parsed.url.hostname.endsWith(`.${domain}`);
411
- });
414
+ } else if (
415
+ options.allowedIframeHostnames ||
416
+ options.allowedIframeDomains
417
+ ) {
418
+ const allowedHostname = (options.allowedIframeHostnames || [])
419
+ .find(function (hostname) {
420
+ return hostname === parsed.url.hostname;
421
+ });
422
+ const allowedDomain = (options.allowedIframeDomains || [])
423
+ .find(function(domain) {
424
+ return parsed.url.hostname === domain || parsed.url.hostname.endsWith(`.${domain}`);
425
+ });
412
426
  allowed = allowedHostname || allowedDomain;
413
427
  }
414
428
  } catch (e) {
@@ -462,9 +476,17 @@ function sanitizeHtml(html, options, _recursing) {
462
476
  return t;
463
477
  });
464
478
  if (allowedSpecificClasses && allowedWildcardClasses) {
465
- value = filterClasses(value, deepmerge(allowedSpecificClasses, allowedWildcardClasses), allowedClassesGlobs);
479
+ value = filterClasses(
480
+ value,
481
+ deepmerge(allowedSpecificClasses, allowedWildcardClasses),
482
+ allowedClassesGlobs
483
+ );
466
484
  } else {
467
- value = filterClasses(value, allowedSpecificClasses || allowedWildcardClasses, allowedClassesGlobs);
485
+ value = filterClasses(
486
+ value,
487
+ allowedSpecificClasses || allowedWildcardClasses,
488
+ allowedClassesGlobs
489
+ );
468
490
  }
469
491
  if (!value.length) {
470
492
  delete frame.attribs[a];
@@ -475,7 +497,10 @@ function sanitizeHtml(html, options, _recursing) {
475
497
  if (options.parseStyleAttributes) {
476
498
  try {
477
499
  const abstractSyntaxTree = postcssParse(name + ' {' + value + '}', { map: false });
478
- const filteredAST = filterCss(abstractSyntaxTree, options.allowedStyles);
500
+ const filteredAST = filterCss(
501
+ abstractSyntaxTree,
502
+ options.allowedStyles
503
+ );
479
504
 
480
505
  value = stringifyStyleAttributes(filteredAST);
481
506
 
@@ -541,6 +566,11 @@ function sanitizeHtml(html, options, _recursing) {
541
566
  // your concern, don't allow them. The same is essentially true for style tags
542
567
  // which have their own collection of XSS vectors.
543
568
  result += text;
569
+ } else if ((options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') && (nonTextTagsArray.indexOf(tag) !== -1)) {
570
+ // htmlparser2 does not decode entities inside raw text elements like
571
+ // textarea and option. The text is already properly encoded, so pass
572
+ // it through without additional escaping to avoid double-encoding.
573
+ result += text;
544
574
  } else if (!addedText) {
545
575
  const escaped = escapeHtml(text, false);
546
576
  if (options.textFilter) {
@@ -608,7 +638,8 @@ function sanitizeHtml(html, options, _recursing) {
608
638
  tempResult = '';
609
639
  }
610
640
  // remove the opening tag from the result
611
- result = result.substring(0, frame.tagPosition) + result.substring(frame.tagPosition + frame.openingTagLength);
641
+ result = result.substring(0, frame.tagPosition) +
642
+ result.substring(frame.tagPosition + frame.openingTagLength);
612
643
  return;
613
644
  } else if (filterResult) {
614
645
  result = result.substring(0, frame.tagPosition);
@@ -643,6 +674,17 @@ function sanitizeHtml(html, options, _recursing) {
643
674
  parser.write(html);
644
675
  parser.end();
645
676
 
677
+ if (options.disallowedTagsMode === 'escape' || options.disallowedTagsMode === 'recursiveEscape') {
678
+ const lastParsedIndex = parser.endIndex;
679
+ if (lastParsedIndex != null && lastParsedIndex >= 0 &&
680
+ lastParsedIndex < html.length) {
681
+ const unparsed = html.substring(lastParsedIndex);
682
+ result += escapeHtml(unparsed);
683
+ } else if ((lastParsedIndex == null || lastParsedIndex < 0) && html.length > 0 && result === '') {
684
+ result = escapeHtml(html);
685
+ }
686
+ }
687
+
646
688
  return result;
647
689
 
648
690
  function initializeState() {
@@ -753,8 +795,10 @@ function sanitizeHtml(html, options, _recursing) {
753
795
  * Modifies the abstractSyntaxTree object.
754
796
  *
755
797
  * @param {object} abstractSyntaxTree - Object representation of CSS attributes.
756
- * @property {array[Declaration]} abstractSyntaxTree.nodes[0] - Each object cointains prop and value key, i.e { prop: 'color', value: 'red' }.
757
- * @param {object} allowedStyles - Keys are properties (i.e color), value is list of permitted regex rules (i.e /green/i).
798
+ * @property {array[Declaration]} abstractSyntaxTree.nodes[0] -
799
+ * Each object contains prop and value key, i.e { prop: 'color', value: 'red' }.
800
+ * @param {object} allowedStyles - Keys are properties (i.e color),
801
+ * value is list of permitted regex rules (i.e /green/i).
758
802
  * @return {object} - The modified tree.
759
803
  */
760
804
  function filterCss(abstractSyntaxTree, allowedStyles) {
@@ -776,7 +820,8 @@ function sanitizeHtml(html, options, _recursing) {
776
820
  }
777
821
 
778
822
  if (selectedRule) {
779
- abstractSyntaxTree.nodes[0].nodes = astRules.nodes.reduce(filterDeclarations(selectedRule), []);
823
+ abstractSyntaxTree.nodes[0].nodes = astRules.nodes
824
+ .reduce(filterDeclarations(selectedRule), []);
780
825
  }
781
826
 
782
827
  return abstractSyntaxTree;
@@ -787,7 +832,8 @@ function sanitizeHtml(html, options, _recursing) {
787
832
  * values in the inline style attribute format.
788
833
  *
789
834
  * @param {AbstractSyntaxTree} filteredAST
790
- * @return {string} - Example: "color:yellow;text-align:center !important;font-family:helvetica;"
835
+ * @return {string} - Example:
836
+ * "color:yellow;text-align:center !important;font-family:helvetica;"
791
837
  */
792
838
  function stringifyStyleAttributes(filteredAST) {
793
839
  return filteredAST.nodes[0].nodes
@@ -804,21 +850,25 @@ function sanitizeHtml(html, options, _recursing) {
804
850
  * Filters the existing attributes for the given property. Discards any attributes
805
851
  * which don't match the allowlist.
806
852
  *
807
- * @param {object} selectedRule - Example: { color: red, font-family: helvetica }
808
- * @param {array} allowedDeclarationsList - List of declarations which pass the allowlist.
809
- * @param {object} attributeObject - Object representing the current css property.
810
- * @property {string} attributeObject.type - Typically 'declaration'.
811
- * @property {string} attributeObject.prop - The CSS property, i.e 'color'.
812
- * @property {string} attributeObject.value - The corresponding value to the css property, i.e 'red'.
813
- * @return {function} - When used in Array.reduce, will return an array of Declaration objects
853
+ * @param {object} selectedRule - Example: { color: red, font-family: helvetica }
854
+ * @param {array} allowedDeclarationsList - List of declarations
855
+ * which pass the allowlist.
856
+ * @param {object} attributeObject - Object representing the current css property.
857
+ * @property {string} attributeObject.type - Typically 'declaration'.
858
+ * @property {string} attributeObject.prop - The CSS property, i.e 'color'.
859
+ * @property {string} attributeObject.value - The corresponding value to
860
+ * the css property, i.e 'red'.
861
+ * @return {function} - When used in Array.reduce,
862
+ * will return an array of Declaration objects
814
863
  */
815
864
  function filterDeclarations(selectedRule) {
816
865
  return function (allowedDeclarationsList, attributeObject) {
817
866
  // If this property is allowlisted...
818
867
  if (has(selectedRule, attributeObject.prop)) {
819
- const matchesRegex = selectedRule[attributeObject.prop].some(function(regularExpression) {
820
- return regularExpression.test(attributeObject.value);
821
- });
868
+ const matchesRegex = selectedRule[attributeObject.prop]
869
+ .some(function(regularExpression) {
870
+ return regularExpression.test(attributeObject.value);
871
+ });
822
872
 
823
873
  if (matchesRegex) {
824
874
  allowedDeclarationsList.push(attributeObject);
@@ -950,7 +1000,7 @@ sanitizeHtml.simpleTransform = function(newTagName, newAttribs, merge) {
950
1000
 
951
1001
  return {
952
1002
  tagName: newTagName,
953
- attribs: attribs
1003
+ attribs
954
1004
  };
955
1005
  };
956
1006
  };
package/package.json CHANGED
@@ -1,19 +1,18 @@
1
1
  {
2
2
  "name": "sanitize-html",
3
- "version": "2.17.0",
3
+ "version": "2.17.2",
4
4
  "description": "Clean up user-submitted HTML, preserving allowlisted elements and allowlisted attributes on a per-element basis",
5
5
  "sideEffects": false,
6
6
  "main": "index.js",
7
7
  "files": [
8
8
  "index.js"
9
9
  ],
10
- "scripts": {
11
- "test": "npx eslint . && mocha test/test.js"
12
- },
13
10
  "repository": {
14
11
  "type": "git",
15
- "url": "https://github.com/apostrophecms/sanitize-html.git"
12
+ "url": "https://github.com/apostrophecms/apostrophe.git",
13
+ "directory": "packages/sanitize-html"
16
14
  },
15
+ "homepage": "https://github.com/apostrophecms/apostrophe/tree/main/packages/sanitize-html#readme",
17
16
  "keywords": [
18
17
  "html",
19
18
  "parser",
@@ -25,20 +24,22 @@
25
24
  "dependencies": {
26
25
  "deepmerge": "^4.2.2",
27
26
  "escape-string-regexp": "^4.0.0",
28
- "htmlparser2": "^8.0.0",
27
+ "htmlparser2": "^10.1.0",
29
28
  "is-plain-object": "^5.0.0",
30
29
  "parse-srcset": "^1.0.2",
31
30
  "postcss": "^8.3.11"
32
31
  },
33
32
  "devDependencies": {
34
- "eslint": "^7.3.1",
35
- "eslint-config-apostrophe": "^3.4.0",
36
- "eslint-config-standard": "^14.1.1",
37
- "eslint-plugin-import": "^2.25.2",
38
- "eslint-plugin-node": "^11.1.0",
39
- "eslint-plugin-promise": "^4.2.1",
40
- "eslint-plugin-standard": "^4.0.1",
33
+ "eslint": "^9.39.1",
41
34
  "mocha": "^10.2.0",
42
- "sinon": "^9.0.2"
35
+ "sinon": "^9.0.2",
36
+ "eslint-config-apostrophe": "^6.0.2"
37
+ },
38
+ "apostropheTestConfig": {
39
+ "requiresMongo": false
40
+ },
41
+ "scripts": {
42
+ "test": "npm run lint && mocha",
43
+ "lint": "eslint ."
43
44
  }
44
45
  }