Repository: apostrophecms/sanitize-html
Pull request #656 (2024-02-27 20:04:11)

added option disallowedTagsMode: 'completelyDiscard'
Added disallowedTagsMode: 'completelyDiscard' option to remove content from disallowed tags.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46178f5..d9c72ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
 
 - Documentation update regarding minimum supported TypeScript version.
 
+- Added disallowedTagsMode: 'completelyDiscard' option to remove the content also in html
+
 ## 2.12.1 (2024-02-22)
 
 - Do not parse sourcemaps in `post-css`. This fixes a vulnerability in which information about the existence or non-existence of files on a server could be disclosed via properly crafted HTML input when the `style` attribute is allowed by the configuration. Thanks to the [Snyk Security team](https://snyk.io/) for the disclosure and to [Dylan Armstrong](https://dylan.is/) for the fix.
diff --git a/README.md b/README.md
index 4a6b44b..5640336 100644
--- a/README.md
+++ b/README.md
@@ -245,6 +245,8 @@ allowedAttributes: {}
 
 If you set `disallowedTagsMode` to `discard` (the default), disallowed tags are discarded. Any text content or subtags are still included, depending on whether the individual subtags are allowed.
 
+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.
+
 If you set `disallowedTagsMode` to `escape`, the disallowed tags are escaped rather than discarded. Any text or subtags are handled normally.
 
 If you set `disallowedTagsMode` to `recursiveEscape`, the disallowed tags are escaped rather than discarded, and the same treatment is applied to all subtags, whether otherwise allowed or not.
@@ -705,7 +707,36 @@ disallowedTagsMode: 'escape'
 
 This will transform `<disallowed>content</disallowed>` to `&lt;disallowed&gt;content&lt;/disallowed&gt;`
 
-Valid values are: `'discard'` (default), `'escape'` (escape the tag) and `'recursiveEscape'` (to escape the tag and all its content).
+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).
+
+#### Discard disallowed but but the inner content of disallowed tags is kept.
+
+If you set `disallowedTagsMode` to `discard`, disallowed tags are discarded but don't remove inner content of disallowed tags.
+
+```js
+disallowedTagsMode: 'discard'
+```
+This will transform `<disallowed>content</disallowed>` to `content`
+
+#### Discard entire content of a disallowed tag
+
+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.
+
+```js
+disallowedTagsMode: 'completelyDiscard'
+```
+
+This will transform `<disallowed>content <allowed>content</allowed> </disallowed>` to `<allowed>content</allowed>`
+
+#### Escape the disallowed tag and all its children even for allowed tags.
+
+if you set `disallowedTagsMode` to `recursiveEscape`, disallowed tag and its children will be escaped even for allowed tags
+
+```js
+disallowedTagsMode: `recursiveEscape`
+```
+
+This will transform `<disallowed>hello<p>world</p></disallowed>` to `&lt;disallowed&gt;hello&lt;p&gt;world&lt;/p&gt;&lt;/disallowed&gt;`
 
 ### Ignore style attribute contents
 
diff --git a/index.js b/index.js
index 62404fa..1144ce8 100644
--- a/index.js
+++ b/index.js
@@ -262,7 +262,7 @@ function sanitizeHtml(html, options, _recursing) {
       if (!tagAllowed(name) || (options.disallowedTagsMode === 'recursiveEscape' && !isEmptyObject(skipMap)) || (options.nestingLimit != null && depth >= options.nestingLimit)) {
         skip = true;
         skipMap[depth] = true;
-        if (options.disallowedTagsMode === 'discard') {
+        if (options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') {
           if (nonTextTagsArray.indexOf(name) !== -1) {
             skipText = true;
             skipTextDepth = 1;
@@ -272,7 +272,7 @@ function sanitizeHtml(html, options, _recursing) {
       }
       depth++;
       if (skip) {
-        if (options.disallowedTagsMode === 'discard') {
+        if (options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') {
           // We want the contents but not this tag
           return;
         }
@@ -511,7 +511,9 @@ function sanitizeHtml(html, options, _recursing) {
         text = lastFrame.innerText !== undefined ? lastFrame.innerText : text;
       }
 
-      if (options.disallowedTagsMode === 'discard' && ((tag === 'script') || (tag === 'style'))) {
+      if (options.disallowedTagsMode === 'completelyDiscard' && !tagAllowed(tag)) {
+        text = '';
+      } else if ((options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') && ((tag === 'script') || (tag === 'style'))) {
         // htmlparser2 gives us these as-is. Escaping them ruins the content. Allowing
         // script tags is, by definition, game over for XSS protection, so if that's
         // your concern, don't allow them. The same is essentially true for style tags
@@ -559,7 +561,7 @@ function sanitizeHtml(html, options, _recursing) {
       const skip = skipMap[depth];
       if (skip) {
         delete skipMap[depth];
-        if (options.disallowedTagsMode === 'discard') {
+        if (options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') {
           frame.updateParentNodeText();
           return;
         }


Test patch:
diff --git a/test/test.js b/test/test.js
index 9891a8e..9c87be3 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1667,5 +1667,32 @@ describe('sanitizeHtml', function() {
       }
     }), '<a style="background-image:url(&quot;/*# sourceMappingURL=../index.js */&quot;)"></a>');
   });
+  it('should completely remove disallowed tags with nested content', () => {
+    const inputHtml = '<div>Some Text<p>Allowed content</p><script>var x = "Disallowed script";</script><span>More allowed content</span> Another Text</div>';
+    const expectedOutput = '<p>Allowed content</p><span>More allowed content</span>';
+    const sanitizedHtml = sanitizeHtml(inputHtml, {
+      allowedTags: [ 'p', 'span' ],
+      disallowedTagsMode: 'completelyDiscard'
+    });
+    assert.equal(sanitizedHtml, expectedOutput);
+  });
+  it('should remove top level tag\'s content', () => {
+    const inputHtml = 'Some Text<p>paragraph content</p> content';
+    const expectedOutput = '<p>paragraph content</p>';
+    const sanitizedHtml = sanitizeHtml(inputHtml, {
+      allowedTags: [ 'p' ],
+      disallowedTagsMode: 'completelyDiscard'
+    });
+    assert.equal(sanitizedHtml, expectedOutput);
+  });
+  it('should completely remove disallowed tag with unclosed tag', () => {
+    const inputHtml = '<div>Some Text<p>paragraph content</p>some text';
+    const expectedOutput = '<p>paragraph content</p>';
+    const sanitizedHtml = sanitizeHtml(inputHtml, {
+      allowedTags: [ 'p' ],
+      disallowedTagsMode: 'completelyDiscard'
+    });
 
+    assert.equal(sanitizedHtml, expectedOutput);
+  });
 });