Repository: apostrophecms/sanitize-html
Pull request #596 (2022-12-17 11:56:08)

feat(parseStyleAttributes): add option to skip style parsing [fix 547]
This will fix https://github.com/apostrophecms/sanitize-html/issues/547

This PR introduces a new option:
```
options.parseStyleAttributes: boolean
```

By default set to true, to match the current behavior. But a user can set it to false, to skip parsing style tags.
This can avoid issues when the package is used in the browser.

```javascript
  it('Should ignore styles when options.parseStyleAttributes is false', function() {
    assert.equal(
      sanitizeHtml('<span style=\'color: blue; text-align: justify\'></span>', {
        allowedTags: false,
        allowedAttributes: {
          span: [ 'style' ]
        },
        allowedStyles: {
          span: {
            color: [ /blue/ ],
            'text-align': [ /left/ ]
          }
        },
        parseStyleAttributes: false
      }), '<span style="color: blue; text-align: justify"></span>'
    );
  });
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 635533d..cf07966 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## UNRELEASED
 
 - If the argument is a number, convert it to a string, for backwards compatibility. Thanks to [Alexander Schranz](https://github.com/alexander-schranz).
+- Add option parseStyleAttributes to skip style parsing. This fixes [issue #547](https://github.com/apostrophecms/sanitize-html/issues/547). Thanks to [Bert Verhelst](https://github.com/bertyhell).
 
 ## 2.8.0 (2022-12-12)
 
diff --git a/README.md b/README.md
index a68720d..c51512d 100644
--- a/README.md
+++ b/README.md
@@ -139,7 +139,8 @@ allowedSchemes: [ 'http', 'https', 'ftp', 'mailto', 'tel' ],
 allowedSchemesByTag: {},
 allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
 allowProtocolRelative: true,
-enforceHtmlBoundary: false
+enforceHtmlBoundary: false,
+parseStyleAttributes: true
 ```
 
 ### Common use cases
@@ -624,6 +625,24 @@ This will transform `<disallowed>content</disallowed>` to `&lt;disallowed&gt;con
 
 Valid values are: `'discard'` (default), `'escape'` (escape the tag) and `'recursiveEscape'` (to escape the tag and all its content).
 
+### Ignore style attribute contents
+
+Instead of discarding faulty style attributes, you can allow them by disabling the parsing of style attributes:
+
+```js
+parseStyleAttributes: false
+```
+
+This will transform `<div style="invalid-prop: non-existing-value">content</div>` to `<div style="invalid-prop: non-existing-value">content</div>` instead of stripping it: `<div>content</div>`
+
+By default the parseStyleAttributes option is true.
+
+When you disable parsing of the style attribute (`parseStyleAttributes: false`) and you pass in options for the allowedStyles property, an error will be thrown. This combination is not permitted.
+
+we recommend sanitizing content server-side in a Node.js environment, as you cannot trust a browser to sanitize things anyway. Consider what a malicious user could do via the network panel, 
+the browser console, or just by writing scripts that submit content similar to what your JavaScript submits. But if you really need to run it on the client in the browser, 
+you may find you need to disable parseStyleAttributes. This is subject to change as it is [an upstream issue with postcss](https://github.com/postcss/postcss/issues/1727), not sanitize-html itself.
+
 ### Restricting deep nesting
 
 You can limit the depth of HTML tags in the document with the `nestingLimit` option:
diff --git a/index.js b/index.js
index bc7d0d4..c02907c 100644
--- a/index.js
+++ b/index.js
@@ -438,19 +438,24 @@ function sanitizeHtml(html, options, _recursing) {
               }
             }
             if (a === 'style') {
-              try {
-                const abstractSyntaxTree = postcssParse(name + ' {' + value + '}');
-                const filteredAST = filterCss(abstractSyntaxTree, options.allowedStyles);
+              if (options.parseStyleAttributes) {
+                try {
+                  const abstractSyntaxTree = postcssParse(name + ' {' + value + '}');
+                  const filteredAST = filterCss(abstractSyntaxTree, options.allowedStyles);
 
-                value = stringifyStyleAttributes(filteredAST);
+                  value = stringifyStyleAttributes(filteredAST);
 
-                if (value.length === 0) {
+                  if (value.length === 0) {
+                    delete frame.attribs[a];
+                    return;
+                  }
+                } catch (e) {
+                  console.warn('Failed to parse "' + name + ' {' + value + '}' + '", If you\'re running this in a browser, we recommend to disable style parsing: options.parseStyleAttributes: false, since this only works in a node environment due to a postcss dependency, More info: https://github.com/apostrophecms/sanitize-html/issues/547');
                   delete frame.attribs[a];
                   return;
                 }
-              } catch (e) {
-                delete frame.attribs[a];
-                return;
+              } else if (options.allowedStyles) {
+                throw new Error('allowedStyles option cannot be used together with parseStyleAttributes: false.');
               }
             }
             result += ' ' + a;
@@ -818,7 +823,8 @@ sanitizeHtml.defaults = {
   allowedSchemesByTag: {},
   allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
   allowProtocolRelative: true,
-  enforceHtmlBoundary: false
+  enforceHtmlBoundary: false,
+  parseStyleAttributes: true
 };
 
 sanitizeHtml.simpleTransform = function(newTagName, newAttribs, merge) {


Test patch:
diff --git a/test/test.js b/test/test.js
index 72e285f..88ecbea 100644
--- a/test/test.js
+++ b/test/test.js
@@ -956,6 +956,36 @@ describe('sanitizeHtml', function() {
       }), '<span style="color:blue"></span>'
     );
   });
+  it('Should ignore styles when options.parseStyleAttributes is false', function() {
+    assert.equal(
+      sanitizeHtml('<span style=\'color: blue; text-align: justify\'></span>', {
+        allowedTags: false,
+        allowedAttributes: {
+          span: [ 'style' ]
+        },
+        parseStyleAttributes: false
+      }), '<span style="color: blue; text-align: justify"></span>'
+    );
+  });
+  it('Should throw an error if both allowedStyles is set and  && parseStyleAttributes is set to false', function() {
+    try {
+      sanitizeHtml('<span style=\'color: blue; text-align: justify\'></span>', {
+        allowedTags: false,
+        allowedAttributes: {
+          span: ['style']
+        },
+        allowedStyles: {
+          p: {
+            'text-align': [/^justify$/]
+          }
+        },
+        parseStyleAttributes: false
+      });
+      assert(false);
+    } catch (err) {
+      assert.equal(err.message, 'allowedStyles option cannot be used together with parseStyleAttributes: false.');
+    }
+  });
   it('Should support !important styles', function() {
     assert.equal(
       sanitizeHtml('<span style=\'color: blue !important\'></span>', {