sanitize-html 2.16.0 → 2.17.1

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 (3) hide show
  1. package/README.md +10 -1
  2. package/index.js +92 -40
  3. package/package.json +14 -13
package/README.md CHANGED
@@ -745,7 +745,7 @@ This will transform `<disallowed>content <allowed>content</allowed> </disallowed
745
745
 
746
746
  #### Escape the disallowed tag and all its children even for allowed tags.
747
747
 
748
- if you set `disallowedTagsMode` to `recursiveEscape`, disallowed tag and its children will be escaped even for allowed tags
748
+ if you set `disallowedTagsMode` to `recursiveEscape`, disallowed tags and their children will be escaped even for allowed tags:
749
749
 
750
750
  ```js
751
751
  disallowedTagsMode: `recursiveEscape`
@@ -753,6 +753,15 @@ disallowedTagsMode: `recursiveEscape`
753
753
 
754
754
  This will transform `<disallowed>hello<p>world</p></disallowed>` to `&lt;disallowed&gt;hello&lt;p&gt;world&lt;/p&gt;&lt;/disallowed&gt;`
755
755
 
756
+ #### Escape the disallowed tag, including all its attributes.
757
+
758
+ By default, attributes are not preserved when tags are escaped. You can set `preserveEscapedAttributes` to `true` to
759
+ keep the attributes, which will also be escaped and therefore have no effect on the browser.
760
+
761
+ ```js
762
+ preserveEscapedAttributes: true
763
+ ```
764
+
756
765
  ### Ignore style attribute contents
757
766
 
758
767
  Instead of discarding faulty style attributes, you can allow them by disabling the parsing of style attributes:
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
@@ -300,7 +302,15 @@ function sanitizeHtml(html, options, _recursing) {
300
302
  }
301
303
  }
302
304
 
303
- if (!allowedAttributesMap || has(allowedAttributesMap, name) || allowedAttributesMap['*']) {
305
+ const isBeingEscaped = skip && (options.disallowedTagsMode === 'escape' || options.disallowedTagsMode === 'recursiveEscape');
306
+ const shouldPreserveEscapedAttributes = isBeingEscaped &&
307
+ options.preserveEscapedAttributes;
308
+
309
+ if (shouldPreserveEscapedAttributes) {
310
+ each(attribs, function(value, a) {
311
+ result += ' ' + a + '="' + escapeHtml((value || ''), true) + '"';
312
+ });
313
+ } else if (!allowedAttributesMap || has(allowedAttributesMap, name) || allowedAttributesMap['*']) {
304
314
  each(attribs, function(value, a) {
305
315
  if (!VALID_HTML_ATTRIBUTE_NAME.test(a)) {
306
316
  // This prevents part of an attribute name in the output from being
@@ -308,8 +318,10 @@ function sanitizeHtml(html, options, _recursing) {
308
318
  delete frame.attribs[a];
309
319
  return;
310
320
  }
311
- // If the value is empty, check if the attribute is in the allowedEmptyAttributes array.
312
- // 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
313
325
  // List taken from https://html.spec.whatwg.org/multipage/indices.html#attributes-3
314
326
  if (value === '' && (!options.allowedEmptyAttributes.includes(a)) &&
315
327
  (options.nonBooleanAttributes.includes(a) || options.nonBooleanAttributes.includes('*'))) {
@@ -320,9 +332,11 @@ function sanitizeHtml(html, options, _recursing) {
320
332
  // as necessary if there are specific values defined.
321
333
  let passedAllowedAttributesMapCheck = false;
322
334
  if (!allowedAttributesMap ||
323
- (has(allowedAttributesMap, name) && allowedAttributesMap[name].indexOf(a) !== -1) ||
335
+ (has(allowedAttributesMap, name) &&
336
+ allowedAttributesMap[name].indexOf(a) !== -1) ||
324
337
  (allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1) ||
325
- (has(allowedAttributesGlobMap, name) && allowedAttributesGlobMap[name].test(a)) ||
338
+ (has(allowedAttributesGlobMap, name) &&
339
+ allowedAttributesGlobMap[name].test(a)) ||
326
340
  (allowedAttributesGlobMap['*'] && allowedAttributesGlobMap['*'].test(a))) {
327
341
  passedAllowedAttributesMapCheck = true;
328
342
  } else if (allowedAttributesMap && allowedAttributesMap[name]) {
@@ -366,12 +380,14 @@ function sanitizeHtml(html, options, _recursing) {
366
380
  const parsed = parseUrl(value);
367
381
 
368
382
  if (options.allowedScriptHostnames || options.allowedScriptDomains) {
369
- const allowedHostname = (options.allowedScriptHostnames || []).find(function (hostname) {
370
- return hostname === parsed.url.hostname;
371
- });
372
- const allowedDomain = (options.allowedScriptDomains || []).find(function(domain) {
373
- return parsed.url.hostname === domain || parsed.url.hostname.endsWith(`.${domain}`);
374
- });
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
+ });
375
391
  allowed = allowedHostname || allowedDomain;
376
392
  }
377
393
  } catch (e) {
@@ -395,13 +411,18 @@ function sanitizeHtml(html, options, _recursing) {
395
411
  allowed = has(options, 'allowIframeRelativeUrls')
396
412
  ? options.allowIframeRelativeUrls
397
413
  : (!options.allowedIframeHostnames && !options.allowedIframeDomains);
398
- } else if (options.allowedIframeHostnames || options.allowedIframeDomains) {
399
- const allowedHostname = (options.allowedIframeHostnames || []).find(function (hostname) {
400
- return hostname === parsed.url.hostname;
401
- });
402
- const allowedDomain = (options.allowedIframeDomains || []).find(function(domain) {
403
- return parsed.url.hostname === domain || parsed.url.hostname.endsWith(`.${domain}`);
404
- });
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
+ });
405
426
  allowed = allowedHostname || allowedDomain;
406
427
  }
407
428
  } catch (e) {
@@ -455,9 +476,17 @@ function sanitizeHtml(html, options, _recursing) {
455
476
  return t;
456
477
  });
457
478
  if (allowedSpecificClasses && allowedWildcardClasses) {
458
- value = filterClasses(value, deepmerge(allowedSpecificClasses, allowedWildcardClasses), allowedClassesGlobs);
479
+ value = filterClasses(
480
+ value,
481
+ deepmerge(allowedSpecificClasses, allowedWildcardClasses),
482
+ allowedClassesGlobs
483
+ );
459
484
  } else {
460
- value = filterClasses(value, allowedSpecificClasses || allowedWildcardClasses, allowedClassesGlobs);
485
+ value = filterClasses(
486
+ value,
487
+ allowedSpecificClasses || allowedWildcardClasses,
488
+ allowedClassesGlobs
489
+ );
461
490
  }
462
491
  if (!value.length) {
463
492
  delete frame.attribs[a];
@@ -468,7 +497,10 @@ function sanitizeHtml(html, options, _recursing) {
468
497
  if (options.parseStyleAttributes) {
469
498
  try {
470
499
  const abstractSyntaxTree = postcssParse(name + ' {' + value + '}', { map: false });
471
- const filteredAST = filterCss(abstractSyntaxTree, options.allowedStyles);
500
+ const filteredAST = filterCss(
501
+ abstractSyntaxTree,
502
+ options.allowedStyles
503
+ );
472
504
 
473
505
  value = stringifyStyleAttributes(filteredAST);
474
506
 
@@ -601,7 +633,8 @@ function sanitizeHtml(html, options, _recursing) {
601
633
  tempResult = '';
602
634
  }
603
635
  // remove the opening tag from the result
604
- result = result.substring(0, frame.tagPosition) + result.substring(frame.tagPosition + frame.openingTagLength);
636
+ result = result.substring(0, frame.tagPosition) +
637
+ result.substring(frame.tagPosition + frame.openingTagLength);
605
638
  return;
606
639
  } else if (filterResult) {
607
640
  result = result.substring(0, frame.tagPosition);
@@ -636,6 +669,16 @@ function sanitizeHtml(html, options, _recursing) {
636
669
  parser.write(html);
637
670
  parser.end();
638
671
 
672
+ if (options.disallowedTagsMode === 'escape' || options.disallowedTagsMode === 'recursiveEscape') {
673
+ const lastParsedIndex = parser.endIndex;
674
+ if (lastParsedIndex != null && lastParsedIndex >= 0 && lastParsedIndex < html.length) {
675
+ const unparsed = html.substring(lastParsedIndex);
676
+ result += escapeHtml(unparsed);
677
+ } else if ((lastParsedIndex == null || lastParsedIndex < 0) && html.length > 0 && result === '') {
678
+ result = escapeHtml(html);
679
+ }
680
+ }
681
+
639
682
  return result;
640
683
 
641
684
  function initializeState() {
@@ -746,8 +789,10 @@ function sanitizeHtml(html, options, _recursing) {
746
789
  * Modifies the abstractSyntaxTree object.
747
790
  *
748
791
  * @param {object} abstractSyntaxTree - Object representation of CSS attributes.
749
- * @property {array[Declaration]} abstractSyntaxTree.nodes[0] - Each object cointains prop and value key, i.e { prop: 'color', value: 'red' }.
750
- * @param {object} allowedStyles - Keys are properties (i.e color), value is list of permitted regex rules (i.e /green/i).
792
+ * @property {array[Declaration]} abstractSyntaxTree.nodes[0] -
793
+ * Each object contains prop and value key, i.e { prop: 'color', value: 'red' }.
794
+ * @param {object} allowedStyles - Keys are properties (i.e color),
795
+ * value is list of permitted regex rules (i.e /green/i).
751
796
  * @return {object} - The modified tree.
752
797
  */
753
798
  function filterCss(abstractSyntaxTree, allowedStyles) {
@@ -769,7 +814,8 @@ function sanitizeHtml(html, options, _recursing) {
769
814
  }
770
815
 
771
816
  if (selectedRule) {
772
- abstractSyntaxTree.nodes[0].nodes = astRules.nodes.reduce(filterDeclarations(selectedRule), []);
817
+ abstractSyntaxTree.nodes[0].nodes = astRules.nodes
818
+ .reduce(filterDeclarations(selectedRule), []);
773
819
  }
774
820
 
775
821
  return abstractSyntaxTree;
@@ -780,7 +826,8 @@ function sanitizeHtml(html, options, _recursing) {
780
826
  * values in the inline style attribute format.
781
827
  *
782
828
  * @param {AbstractSyntaxTree} filteredAST
783
- * @return {string} - Example: "color:yellow;text-align:center !important;font-family:helvetica;"
829
+ * @return {string} - Example:
830
+ * "color:yellow;text-align:center !important;font-family:helvetica;"
784
831
  */
785
832
  function stringifyStyleAttributes(filteredAST) {
786
833
  return filteredAST.nodes[0].nodes
@@ -797,21 +844,25 @@ function sanitizeHtml(html, options, _recursing) {
797
844
  * Filters the existing attributes for the given property. Discards any attributes
798
845
  * which don't match the allowlist.
799
846
  *
800
- * @param {object} selectedRule - Example: { color: red, font-family: helvetica }
801
- * @param {array} allowedDeclarationsList - List of declarations which pass the allowlist.
802
- * @param {object} attributeObject - Object representing the current css property.
803
- * @property {string} attributeObject.type - Typically 'declaration'.
804
- * @property {string} attributeObject.prop - The CSS property, i.e 'color'.
805
- * @property {string} attributeObject.value - The corresponding value to the css property, i.e 'red'.
806
- * @return {function} - When used in Array.reduce, will return an array of Declaration objects
847
+ * @param {object} selectedRule - Example: { color: red, font-family: helvetica }
848
+ * @param {array} allowedDeclarationsList - List of declarations
849
+ * which pass the allowlist.
850
+ * @param {object} attributeObject - Object representing the current css property.
851
+ * @property {string} attributeObject.type - Typically 'declaration'.
852
+ * @property {string} attributeObject.prop - The CSS property, i.e 'color'.
853
+ * @property {string} attributeObject.value - The corresponding value to
854
+ * the css property, i.e 'red'.
855
+ * @return {function} - When used in Array.reduce,
856
+ * will return an array of Declaration objects
807
857
  */
808
858
  function filterDeclarations(selectedRule) {
809
859
  return function (allowedDeclarationsList, attributeObject) {
810
860
  // If this property is allowlisted...
811
861
  if (has(selectedRule, attributeObject.prop)) {
812
- const matchesRegex = selectedRule[attributeObject.prop].some(function(regularExpression) {
813
- return regularExpression.test(attributeObject.value);
814
- });
862
+ const matchesRegex = selectedRule[attributeObject.prop]
863
+ .some(function(regularExpression) {
864
+ return regularExpression.test(attributeObject.value);
865
+ });
815
866
 
816
867
  if (matchesRegex) {
817
868
  allowedDeclarationsList.push(attributeObject);
@@ -923,7 +974,8 @@ sanitizeHtml.defaults = {
923
974
  allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
924
975
  allowProtocolRelative: true,
925
976
  enforceHtmlBoundary: false,
926
- parseStyleAttributes: true
977
+ parseStyleAttributes: true,
978
+ preserveEscapedAttributes: false
927
979
  };
928
980
 
929
981
  sanitizeHtml.simpleTransform = function(newTagName, newAttribs, merge) {
@@ -942,7 +994,7 @@ sanitizeHtml.simpleTransform = function(newTagName, newAttribs, merge) {
942
994
 
943
995
  return {
944
996
  tagName: newTagName,
945
- attribs: attribs
997
+ attribs
946
998
  };
947
999
  };
948
1000
  };
package/package.json CHANGED
@@ -1,19 +1,18 @@
1
1
  {
2
2
  "name": "sanitize-html",
3
- "version": "2.16.0",
3
+ "version": "2.17.1",
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",
@@ -31,14 +30,16 @@
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
  }