Repository: apostrophecms/sanitize-html
Pull request #512 (2021-11-06 19:57:31)

allowedClasses support for regex expressions
Using wildcard on allowedClasses poses a security risk. Having regex support on classes is a better solution where you can be more explicit with the whitelisting.

Disclaimer: This is a performant way of allowing Regular Expressions on the existing codebase in a couple of lines :)


Patch:
diff --git a/README.md b/README.md
index 6bc7549..dbf5a58 100644
--- a/README.md
+++ b/README.md
@@ -239,6 +239,7 @@ const clean = sanitizeHtml(dirty, {
 ```
 
 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-*' ],
@@ -246,6 +247,16 @@ allowedClasses: {
 }
 ```
 
+Furthermore, regular expressions are supported too:
+
+```js
+allowedClasses: {
+  p: [ /^regex\d{2}$/ ]
+}
+```
+
+> Note: It is advised to construct your regular expressions by starting with `^` and ending with `$`, like in the example above. This way you allow **only** the classes you declare and not other potentially harmful matches.
+
 ### Allowed CSS Styles
 
 If you wish to allow specific CSS _styles_ on a particular element, you can do that with the `allowedStyles` option. Simply declare your desired attributes as regular expression options within an array for the given attribute. Specific elements will inherit allowlisted attributes from the global (`*`) attribute. Any other CSS classes are discarded.
diff --git a/index.js b/index.js
index 26c9142..ed87028 100644
--- a/index.js
+++ b/index.js
@@ -156,6 +156,7 @@ function sanitizeHtml(html, options, _recursing) {
   }
   const allowedClassesMap = {};
   const allowedClassesGlobMap = {};
+  const allowedClassesRegexMap = {};
   each(options.allowedClasses, function(classes, tag) {
     // Implicitly allows the class attribute
     if (allowedAttributesMap) {
@@ -166,10 +167,13 @@ function sanitizeHtml(html, options, _recursing) {
     }
 
     allowedClassesMap[tag] = [];
+    allowedClassesRegexMap[tag] = [];
     const globRegex = [];
     classes.forEach(function(obj) {
       if (typeof obj === 'string' && obj.indexOf('*') >= 0) {
         globRegex.push(escapeStringRegexp(obj).replace(/\\\*/g, '.*'));
+      } else if (obj instanceof RegExp) {
+        allowedClassesRegexMap[tag].push(obj);
       } else {
         allowedClassesMap[tag].push(obj);
       }
@@ -431,12 +435,16 @@ function sanitizeHtml(html, options, _recursing) {
               const allowedSpecificClasses = allowedClassesMap[name];
               const allowedWildcardClasses = allowedClassesMap['*'];
               const allowedSpecificClassesGlob = allowedClassesGlobMap[name];
+              const allowedSpecificClassesRegex = allowedClassesRegexMap[name];
               const allowedWildcardClassesGlob = allowedClassesGlobMap['*'];
-              const allowedClassesGlobs = [ allowedSpecificClassesGlob, allowedWildcardClassesGlob ].filter(
-                function(t) {
+              const allowedClassesGlobs = [
+                allowedSpecificClassesGlob,
+                allowedWildcardClassesGlob
+              ]
+                .concat(allowedSpecificClassesRegex)
+                .filter(function (t) {
                   return t;
-                }
-              );
+                });
               if (allowedSpecificClasses && allowedWildcardClasses) {
                 value = filterClasses(value, deepmerge(allowedSpecificClasses, allowedWildcardClasses), allowedClassesGlobs);
               } else {


Test patch:
diff --git a/test/test.js b/test/test.js
index 94b254c..9dddc7a 100644
--- a/test/test.js
+++ b/test/test.js
@@ -425,6 +425,20 @@ describe('sanitizeHtml', function() {
       '<p class="nifty simple dippy">whee</p>'
     );
   });
+  it('should allow only classes that matches `allowedClasses` regex', function() {
+    assert.equal(
+      sanitizeHtml(
+        '<p class="nifty33 nifty2 dippy">whee</p>',
+        {
+          allowedTags: [ 'p' ],
+          allowedClasses: {
+            p: [ /^nifty\d{2}$/, /^d\w{4}$/ ]
+          }
+        }
+      ),
+      '<p class="nifty33 dippy">whee</p>'
+    );
+  });
   it('should allow defining schemes on a per-tag basis', function() {
     assert.equal(
       sanitizeHtml(