Repository: apostrophecms/sanitize-html
Pull request #472 (2021-04-25 02:33:49)

Support wildcards in allowedClasses (update to #157)
Support wildcards in `allowedClasses` like this:

```javascript
allowedClasses: {
  p: ["prefix-*"]
}
```

---

Useful especially for allowing the `language-xxx` syntax indicator classes in [highlight.js](https://highlightjs.org/).

Attempts have been made in #157, and here I tried to build up a glob map before the loop for better performance, the same as what was done for allowed Attributes. And the wildcard may match zero character for consistency with allowed Attributes too.

All tests passed.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ed08fd1..5187e08 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
 # Changelog
 
+## 2.4.0 (2021-05-05):
+- Added support for class names with wildcards in `allowedClasses`.
+
 ## 2.3.3 (2021-03-19):
 - Security fix: `allowedSchemes` and related options did not properly block schemes containing a hyphen, plus sign, period or digit, such as `ms-calculator:`. Thanks to Lukas Euler for pointing out the issue.
 - Added a security note about the known risks associated with using the `parser` option, especially `decodeEntities: false`. See the documentation.
diff --git a/README.md b/README.md
index d25dd70..2db6e8b 100644
--- a/README.md
+++ b/README.md
@@ -224,9 +224,10 @@ const clean = sanitizeHtml(dirty, {
 });
 ```
 
-Similar to `allowedAttributes`, you can use `*` as a tag name, to allow listed classes to be valid for any tag:
+Similar to `allowedAttributes`, you can use `*` to allow classes with a certain prefix, or use `*` as a tag name to allow listed classes to be valid for any tag:
 ```js
 allowedClasses: {
+  'code': [ 'language-*', 'lang-*' ],
   '*': [ 'fancy', 'simple' ]
 }
 ```
diff --git a/index.js b/index.js
index 4156c2a..7e2f666 100644
--- a/index.js
+++ b/index.js
@@ -146,10 +146,13 @@ function sanitizeHtml(html, options, _recursing) {
           allowedAttributesMap[tag].push(obj);
         }
       });
-      allowedAttributesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
+      if (globRegex.length) {
+        allowedAttributesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
+      }
     });
   }
   const allowedClassesMap = {};
+  const allowedClassesGlobMap = {};
   each(options.allowedClasses, function(classes, tag) {
     // Implicitly allows the class attribute
     if (allowedAttributesMap) {
@@ -159,7 +162,18 @@ function sanitizeHtml(html, options, _recursing) {
       allowedAttributesMap[tag].push('class');
     }
 
-    allowedClassesMap[tag] = classes;
+    allowedClassesMap[tag] = [];
+    const globRegex = [];
+    classes.forEach(function(obj) {
+      if (typeof obj === 'string' && obj.indexOf('*') >= 0) {
+        globRegex.push(escapeStringRegexp(obj).replace(/\\\*/g, '.*'));
+      } else {
+        allowedClassesMap[tag].push(obj);
+      }
+    });
+    if (globRegex.length) {
+      allowedClassesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
+    }
   });
 
   const transformTagsMap = {};
@@ -379,10 +393,17 @@ function sanitizeHtml(html, options, _recursing) {
             if (a === 'class') {
               const allowedSpecificClasses = allowedClassesMap[name];
               const allowedWildcardClasses = allowedClassesMap['*'];
+              const allowedSpecificClassesGlob = allowedClassesGlobMap[name];
+              const allowedWildcardClassesGlob = allowedClassesGlobMap['*'];
+              const allowedClassesGlobs = [ allowedSpecificClassesGlob, allowedWildcardClassesGlob ].filter(
+                function(t) {
+                  return t;
+                }
+              );
               if (allowedSpecificClasses && allowedWildcardClasses) {
-                value = filterClasses(value, deepmerge(allowedSpecificClasses, allowedWildcardClasses));
+                value = filterClasses(value, deepmerge(allowedSpecificClasses, allowedWildcardClasses), allowedClassesGlobs);
               } else {
-                value = filterClasses(value, allowedSpecificClasses || allowedWildcardClasses);
+                value = filterClasses(value, allowedSpecificClasses || allowedWildcardClasses, allowedClassesGlobs);
               }
               if (!value.length) {
                 delete frame.attribs[a];
@@ -669,14 +690,16 @@ function sanitizeHtml(html, options, _recursing) {
     };
   }
 
-  function filterClasses(classes, allowed) {
+  function filterClasses(classes, allowed, allowedGlobs) {
     if (!allowed) {
       // The class attribute is allowed without filtering on this tag
       return classes;
     }
     classes = classes.split(/\s+/);
     return classes.filter(function(clss) {
-      return allowed.indexOf(clss) !== -1;
+      return allowed.indexOf(clss) !== -1 || allowedGlobs.some(function(glob) {
+        return glob.test(clss);
+      });
     }).join(' ');
   }
 }
diff --git a/package.json b/package.json
index 0db05af..5acbd86 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "sanitize-html",
-  "version": "2.3.3",
+  "version": "2.4.0",
   "description": "Clean up user-submitted HTML, preserving whitelisted elements and whitelisted attributes on a per-element basis",
   "sideEffects": false,
   "main": "index.js",


Test patch:
diff --git a/test/test.js b/test/test.js
index e7d9100..17ec4ce 100644
--- a/test/test.js
+++ b/test/test.js
@@ -384,6 +384,35 @@ describe('sanitizeHtml', function() {
       '<p class="nifty simple">whee</p><div class="simple dippy"></div>'
     );
   });
+  it('should allow classes that match wildcards for a single tag or all tags', function() {
+    assert.equal(
+      sanitizeHtml(
+        '<p class="nifty- nifty-a simple dippy dippy-a-simple">whee</p>',
+        {
+          allowedTags: [ 'p' ],
+          allowedClasses: {
+            '*': [ 'dippy-*-simple' ],
+            p: [ 'nifty-*' ]
+          }
+        }
+      ),
+      '<p class="nifty- nifty-a dippy-a-simple">whee</p>'
+    );
+  });
+  it('should allow all classes if whitelist contains a sigle `*`', function() {
+    assert.equal(
+      sanitizeHtml(
+        '<p class="nifty simple dippy">whee</p>',
+        {
+          allowedTags: [ 'p' ],
+          allowedClasses: {
+            '*': [ '*' ]
+          }
+        }
+      ),
+      '<p class="nifty simple dippy">whee</p>'
+    );
+  });
   it('should allow defining schemes on a per-tag basis', function() {
     assert.equal(
       sanitizeHtml(