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.
- package/README.md +62 -3
- package/index.js +29 -9
- 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 `<disallowed>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
|
|
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
|
|
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
|
|
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 +=
|
|
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
|
|
539
|
+
if (options.textFilter) {
|
|
535
540
|
result += options.textFilter(escaped, tag);
|
|
536
|
-
} else
|
|
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
|
|
588
|
-
|
|
589
|
-
|
|
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', '
|
|
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