sanitize-html 2.14.0 → 2.16.0

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 +62 -3
  2. package/index.js +29 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -494,6 +494,21 @@ sanitizeHtml(
494
494
  );
495
495
  ```
496
496
 
497
+ The filter function can also return the string `"excludeTag"` to only remove the tag, while keeping its content. For example, you can remove tags for anchors with invalid links:
498
+
499
+ ```js
500
+ sanitizeHtml(
501
+ 'This is a <a href="javascript:alert(123)">bad link</a> and a <a href="https://www.linux.org">good link</a>',
502
+ {
503
+ exclusiveFilter: function(frame) {
504
+ // the href attribute is removed by the URL protocol check
505
+ return frame.tag === 'a' && !frame.attribs.href ? 'excludeTag' : false;
506
+ }
507
+ }
508
+ );
509
+ // Output: 'This is a bad link and a <a href="https://www.linux.org">good link</a>'
510
+ ```
511
+
497
512
  The `frame` object supplied to the callback provides the following attributes:
498
513
 
499
514
  - `tag`: The tag name, i.e. `'img'`.
@@ -709,9 +724,9 @@ This will transform `<disallowed>content</disallowed>` to `&lt;disallowed&gt;con
709
724
 
710
725
  Valid values are: `'discard'` (default), `'completelyDiscard'` (remove disallowed tag's content), `'escape'` (escape the tag) and `'recursiveEscape'` (to escape the tag and all its content).
711
726
 
712
- #### Discard disallowed but but the inner content of disallowed tags is kept.
727
+ #### Discard disallowed but the inner content of disallowed tags is kept.
713
728
 
714
- If you set `disallowedTagsMode` to `discard`, disallowed tags are discarded but but the inner content of disallowed tags is kept.
729
+ If you set `disallowedTagsMode` to `discard`, disallowed tags are discarded but the inner content of disallowed tags is kept.
715
730
 
716
731
  ```js
717
732
  disallowedTagsMode: 'discard'
@@ -720,7 +735,7 @@ This will transform `<disallowed>content</disallowed>` to `content`
720
735
 
721
736
  #### Discard entire content of a disallowed tag
722
737
 
723
- If you set `disallowedTagsMode` to `completelyDiscard`, disallowed tags and any content they contain are discarded. Any subtags are still included, as long as those individual subtags are allowed.
738
+ If you set `disallowedTagsMode` to `completelyDiscard`, disallowed tags and any text they contain are discarded. This also discards top-level text. Any subtags are still included, as long as those individual subtags are allowed.
724
739
 
725
740
  ```js
726
741
  disallowedTagsMode: 'completelyDiscard'
@@ -766,6 +781,50 @@ nestingLimit: 6
766
781
 
767
782
  This will prevent the user from nesting tags more than 6 levels deep. Tags deeper than that are stripped out exactly as if they were disallowed. Note that this means text is preserved in the usual ways where appropriate.
768
783
 
784
+ ### Advanced filtering
785
+
786
+ For more advanced filtering you can hook directly into the parsing process using tag open and tag close events.
787
+
788
+ The `onOpenTag` event is triggered when an opening tag is encountered. It has two arguments:
789
+ - `tagName`: The name of the tag.
790
+ - `attribs`: An object containing the tag's attributes, e.g. `{ src: "/path/to/tux.png" }`.
791
+
792
+ The `onCloseTag` event is triggered when a closing tag is encountered. It has the following arguments:
793
+ - `tagName`: The name of the tag.
794
+ - `isImplied`: A boolean indicating whether the closing tag is implied (e.g. `<p>foo<p>bar`) or explicit (e.g. `<p>foo</p><p>bar</p>`).
795
+
796
+ For example, you may want to add spaces around a removed tag, like this:
797
+ ```js
798
+ const allowedTags = [ 'b' ];
799
+ let addSpace = false;
800
+ const sanitizedHtml = sanitizeHtml(
801
+ 'There should be<div><p>spaces</p></div>between <b>these</b> words.',
802
+ {
803
+ allowedTags,
804
+ onOpenTag: (tagName, attribs) => {
805
+ addSpace = !allowedTags.includes(tagName);
806
+ },
807
+ onCloseTag: (tagName, isImplied) => {
808
+ addSpace = !allowedTags.includes(tagName);
809
+ },
810
+ textFilter: (text) => {
811
+ if (addSpace) {
812
+ addSpace = false;
813
+ return ' ' + text;
814
+ }
815
+ return text;
816
+ }
817
+ }
818
+ );
819
+ ```
820
+
821
+ In this example, we are setting a flag when a tag that will be removed has been opened or closed. Then we use the `textFilter` to modify the text to include spaces. The example should produce:
822
+ ```
823
+ There should be spaces between <b>these</b> words.
824
+ ```
825
+
826
+ This is a simplified example that is not meant to be production-ready. For your specific case, you may want to keep track of currently open tags, using the open and close events to push and pop items on the stack, or only insert spaces next to a subset of disallowed tags.
827
+
769
828
  ## About ApostropheCMS
770
829
 
771
830
  sanitize-html was created at [P'unk Avenue](https://punkave.com) for use in [ApostropheCMS](https://apostrophecms.com), an open-source content management system built on Node.js. If you like sanitize-html you should definitely check out ApostropheCMS.
package/index.js CHANGED
@@ -97,6 +97,7 @@ function sanitizeHtml(html, options, _recursing) {
97
97
  this.attribs = attribs || {};
98
98
  this.tagPosition = result.length;
99
99
  this.text = ''; // Node inner text
100
+ this.openingTagLength = 0;
100
101
  this.mediaChildren = [];
101
102
 
102
103
  this.updateParentNodeText = function() {
@@ -219,6 +220,10 @@ function sanitizeHtml(html, options, _recursing) {
219
220
 
220
221
  const parser = new htmlparser.Parser({
221
222
  onopentag: function(name, attribs) {
223
+ if (options.onOpenTag) {
224
+ options.onOpenTag(name, attribs);
225
+ }
226
+
222
227
  // If `enforceHtmlBoundary` is `true` and this has found the opening
223
228
  // `html` tag, reset the state.
224
229
  if (options.enforceHtmlBoundary && name === 'html') {
@@ -268,7 +273,6 @@ function sanitizeHtml(html, options, _recursing) {
268
273
  skipTextDepth = 1;
269
274
  }
270
275
  }
271
- skipMap[depth] = true;
272
276
  }
273
277
  depth++;
274
278
  if (skip) {
@@ -279,7 +283,7 @@ function sanitizeHtml(html, options, _recursing) {
279
283
  if (options.textFilter) {
280
284
  result += options.textFilter(escaped, name);
281
285
  } else {
282
- result += escapeHtml(frame.innerText);
286
+ result += escaped;
283
287
  }
284
288
  addedText = true;
285
289
  }
@@ -507,6 +511,7 @@ function sanitizeHtml(html, options, _recursing) {
507
511
  result = tempResult + escapeHtml(result);
508
512
  tempResult = '';
509
513
  }
514
+ frame.openingTagLength = result.length - frame.tagPosition;
510
515
  },
511
516
  ontext: function(text) {
512
517
  if (skipText) {
@@ -529,11 +534,11 @@ function sanitizeHtml(html, options, _recursing) {
529
534
  // your concern, don't allow them. The same is essentially true for style tags
530
535
  // which have their own collection of XSS vectors.
531
536
  result += text;
532
- } else {
537
+ } else if (!addedText) {
533
538
  const escaped = escapeHtml(text, false);
534
- if (options.textFilter && !addedText) {
539
+ if (options.textFilter) {
535
540
  result += options.textFilter(escaped, tag);
536
- } else if (!addedText) {
541
+ } else {
537
542
  result += escaped;
538
543
  }
539
544
  }
@@ -543,6 +548,9 @@ function sanitizeHtml(html, options, _recursing) {
543
548
  }
544
549
  },
545
550
  onclosetag: function(name, isImplied) {
551
+ if (options.onCloseTag) {
552
+ options.onCloseTag(name, isImplied);
553
+ }
546
554
 
547
555
  if (skipText) {
548
556
  skipTextDepth--;
@@ -584,9 +592,21 @@ function sanitizeHtml(html, options, _recursing) {
584
592
  delete transformMap[depth];
585
593
  }
586
594
 
587
- if (options.exclusiveFilter && options.exclusiveFilter(frame)) {
588
- result = result.substr(0, frame.tagPosition);
589
- return;
595
+ if (options.exclusiveFilter) {
596
+ const filterResult = options.exclusiveFilter(frame);
597
+ if (filterResult === 'excludeTag') {
598
+ if (skip) {
599
+ // no longer escaping the tag since it's not added at all
600
+ result = tempResult;
601
+ tempResult = '';
602
+ }
603
+ // remove the opening tag from the result
604
+ result = result.substring(0, frame.tagPosition) + result.substring(frame.tagPosition + frame.openingTagLength);
605
+ return;
606
+ } else if (filterResult) {
607
+ result = result.substring(0, frame.tagPosition);
608
+ return;
609
+ }
590
610
  }
591
611
 
592
612
  frame.updateParentNodeMediaChildren();
@@ -832,7 +852,7 @@ sanitizeHtml.defaults = {
832
852
  'main', 'nav', 'section',
833
853
  // Text content
834
854
  'blockquote', 'dd', 'div', 'dl', 'dt', 'figcaption', 'figure',
835
- 'hr', 'li', 'main', 'ol', 'p', 'pre', 'ul',
855
+ 'hr', 'li', 'menu', 'ol', 'p', 'pre', 'ul',
836
856
  // Inline text semantics
837
857
  'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn',
838
858
  'em', 'i', 'kbd', 'mark', 'q',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanitize-html",
3
- "version": "2.14.0",
3
+ "version": "2.16.0",
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",