Repository: apostrophecms/sanitize-html
Pull request #480 (2021-05-19 13:22:36)

Allow script tags for specific domains / hostnames
I needed the same feature stated here: https://github.com/apostrophecms/sanitize-html/issues/405

Added allowedScriptHostnames and allowedScriptDomains


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 718cc08..fac3bed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Changelog
 
+## Unreleased:
+
+- New `allowedScriptHostnames` option, it enables you to specify which hostnames are allowed in a script tag.
+- New `allowedScriptDomains` option, it enables you to specify which domains are allowed in a script tag. Thank you to [Yorick Girard](https://github.com/yorickgirard) for this and the `allowedScriptHostnames` contribution.
+
 ## 2.4.0 (2021-05-19):
 - Added support for class names with wildcards in `allowedClasses`. Thanks to [zhangbenber](https://github.com/zhangbenber) for the contribution.
 
diff --git a/README.md b/README.md
index 024aa68..e9b1a90 100644
--- a/README.md
+++ b/README.md
@@ -516,6 +516,32 @@ const clean = sanitizeHtml('<p><iframe src="https://us02web.zoom.us/embed/12345"
 });
 ```
 
+### Script Filters
+
+Similarly to iframes you can allow a script tag on a list of whitelisted domains
+
+```js
+const clean = sanitizeHtml('<script src="https://www.safe.authorized.com/lib.js"></script>', {
+    allowedTags: ['script'],
+    allowedAttributes: {
+        script: ['src']
+    },
+    allowedScriptDomains: ['authorized.com'],
+})
+```
+
+You can allow a script tag on a list of whitelisted hostnames too
+
+```js
+const clean = sanitizeHtml('<script src="https://www.authorized.com/lib.js"></script>', {
+    allowedTags: ['script'],
+    allowedAttributes: {
+        script: ['src']
+    },
+    allowedScriptHostnames: [ 'www.authorized.com' ],
+})
+```
+
 ### Allowed URL schemes
 
 By default, we allow the following URL schemes in cases where `href`, `src`, etc. are allowed:
diff --git a/index.js b/index.js
index 7e2f666..6916cbd 100644
--- a/index.js
+++ b/index.js
@@ -315,6 +315,33 @@ function sanitizeHtml(html, options, _recursing) {
                 return;
               }
             }
+            if (name === 'script' && a === 'src') {
+              let allowed = true;
+
+              frame.innerText = '';
+
+              try {
+                const parsed = new URL(value);
+
+                if (options.allowedScriptHostnames || options.allowedScriptDomains) {
+                  const allowedHostname = (options.allowedScriptHostnames || []).find(function (hostname) {
+                    return hostname === parsed.hostname;
+                  });
+                  const allowedDomain = (options.allowedScriptDomains || []).find(function(domain) {
+                    return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
+                  });
+                  allowed = allowedHostname || allowedDomain;
+                }
+              } catch (e) {
+                allowed = false;
+              }
+
+              if (!allowed) {
+                delete frame.attribs[a];
+                return;
+              }
+            }
+
             if (name === 'iframe' && a === 'src') {
               let allowed = true;
               try {


Test patch:
diff --git a/test/test.js b/test/test.js
index 17ec4ce..6046af6 100644
--- a/test/test.js
+++ b/test/test.js
@@ -893,6 +893,57 @@ describe('sanitizeHtml', function() {
       }), '<span style="color:yellow;text-align:center;font-family:helvetica"></span>'
     );
   });
+  it('should delete the script tag', function() {
+    assert.equal(sanitizeHtml('<script src="https://www.unauthorized.com/lib.js"></script>', {
+      allowedTags: [ 'script' ],
+      allowVulnerableTags: true,
+      allowedAttributes: {
+        script: [ 'src' ]
+      },
+      allowedScriptHostnames: [ 'www.authorized.com' ]
+    }), '<script></script>');
+  });
+  it('should delete the script tag since src is not a valid URL', function() {
+    assert.equal(sanitizeHtml('<script src="not-a-valid-url"></script>', {
+      allowedTags: [ 'script' ],
+      allowVulnerableTags: true,
+      allowedAttributes: {
+        script: [ 'src' ]
+      },
+      allowedScriptHostnames: [ 'www.unauthorized.com' ]
+    }), '<script></script>');
+  });
+  it('Should allow domains in a script that are whitelisted', function() {
+    assert.equal(
+      sanitizeHtml('<script src="https://www.safe.authorized.com/lib.js"></script>', {
+        allowedTags: [ 'script' ],
+        allowedAttributes: {
+          script: [ 'src' ]
+        },
+        allowedScriptDomains: [ 'authorized.com' ]
+      }), '<script src="https://www.safe.authorized.com/lib.js"></script>'
+    );
+  });
+  it('should delete the script tag content', function() {
+    assert.equal(sanitizeHtml('<script src="https://www.authorized.com/lib.js"> alert("evil") </script>', {
+      allowedTags: [ 'script' ],
+      allowVulnerableTags: true,
+      allowedAttributes: {
+        script: [ 'src' ]
+      },
+      allowedScriptHostnames: [ 'www.authorized.com' ]
+    }), '<script src="https://www.authorized.com/lib.js"></script>');
+  });
+  it('Should allow hostnames in a script that are whitelisted', function() {
+    assert.equal(sanitizeHtml('<script src="https://www.authorized.com/lib.js"></script>', {
+      allowedTags: [ 'script' ],
+      allowVulnerableTags: true,
+      allowedAttributes: {
+        script: [ 'src' ]
+      },
+      allowedScriptHostnames: [ 'www.authorized.com' ]
+    }), '<script src="https://www.authorized.com/lib.js"></script>');
+  });
   it('Should allow hostnames in an iframe that are whitelisted', function() {
     assert.equal(
       sanitizeHtml('<iframe src="https://www.youtube.com/embed/c2IlcS7AHxM"></iframe>', {