diff --git a/package.json b/package.json index 27380c5..b9fd9a5 100644 --- a/package.json +++ b/package.json @@ -26,13 +26,13 @@ "license": "MIT", "dependencies": { "chalk": "^2.4.1", + "css-tree": "^1.0.0-alpha.29", "htmlparser2": "^3.10.0", "lodash.clonedeep": "^4.5.0", "lodash.escaperegexp": "^4.1.2", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.mergewith": "^4.6.1", - "postcss": "^7.0.5", "srcset": "^1.0.0", "xtend": "^4.0.1" }, diff --git a/src/index.js b/src/index.js index 0376822..569fdac 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,7 @@ var mergeWith = require('lodash.mergewith'); var isString = require('lodash.isstring'); var isPlainObject = require('lodash.isplainobject'); var srcset = require('srcset'); -var postcss = require('postcss'); +var csstree = require('css-tree'); var url = require('url'); function each(obj, cb) { @@ -299,15 +299,31 @@ function sanitizeHtml(html, options, _recursing) { } if (a === 'style') { try { - var abstractSyntaxTree = postcss.parse(name + " {" + value + "}"); - var filteredAST = filterCss(abstractSyntaxTree, options.allowedStyles); + var ast = csstree.parse(name + " {" + value + "}"); + var selectors = rulesForSelector(name, options.allowedStyles || {}); - value = stringifyStyleAttributes(filteredAST); + csstree.walk(ast, function(node, item, list) { + if (node.type === 'Declaration' && list) { + var value = csstree.generate(node.value); + var rules = selectors[node.property]; + + if (rules !== undefined && rules.every(function (rule) { return !value.match(rule); })) { + list.remove(item) + } + } + }) + + value = csstree.generate(ast).slice(name.length + 1); + value = value.slice(0, value.length - 1); if(value.length === 0) { delete frame.attribs[a]; return; } + + // preserve the final semicolon + value += ';'; + } catch (e) { delete frame.attribs[a]; return; @@ -465,27 +481,11 @@ function sanitizeHtml(html, options, _recursing) { return !options.allowedSchemes || options.allowedSchemes.indexOf(scheme) === -1; } - /** - * Filters user input css properties by whitelisted regex attributes. - * - * @param {object} abstractSyntaxTree - Object representation of CSS attributes. - * @property {array[Declaration]} abstractSyntaxTree.nodes[0] - Each object cointains prop and value key, i.e { prop: 'color', value: 'red' }. - * @param {object} allowedStyles - Keys are properties (i.e color), value is list of permitted regex rules (i.e /green/i). - * @return {object} - Abstract Syntax Tree with filtered style attributes. - */ - function filterCss(abstractSyntaxTree, allowedStyles) { - if (!allowedStyles) { - return abstractSyntaxTree; - } - - var filteredAST = cloneDeep(abstractSyntaxTree); - var astRules = abstractSyntaxTree.nodes[0]; - var selectedRule; - + function rulesForSelector(selector, allowedStyles) { // Merge global and tag-specific styles into new AST. - if (allowedStyles[astRules.selector] && allowedStyles['*']) { - selectedRule = mergeWith( - cloneDeep(allowedStyles[astRules.selector]), + if (allowedStyles[selector] && allowedStyles['*']) { + return mergeWith( + cloneDeep(allowedStyles[selector]), allowedStyles['*'], function(objValue, srcValue) { if (Array.isArray(objValue)) { @@ -493,61 +493,9 @@ function sanitizeHtml(html, options, _recursing) { } } ); - } else { - selectedRule = allowedStyles[astRules.selector] || allowedStyles['*']; } - if (selectedRule) { - filteredAST.nodes[0].nodes = astRules.nodes.reduce(filterDeclarations(selectedRule), []); - } - - return filteredAST; - } - - /** - * Extracts the style attribues from an AbstractSyntaxTree and formats those - * values in the inline style attribute format. - * - * @param {AbstractSyntaxTree} filteredAST - * @return {string} - Example: "color:yellow;text-align:center;font-family:helvetica;" - */ - function stringifyStyleAttributes(filteredAST) { - return filteredAST.nodes[0].nodes - .reduce(function(extractedAttributes, attributeObject) { - extractedAttributes.push( - attributeObject.prop + ':' + attributeObject.value + ';' - ); - return extractedAttributes; - }, []) - .join(''); - } - - /** - * Filters the existing attributes for the given property. Discards any attributes - * which don't match the whitelist. - * - * @param {object} selectedRule - Example: { color: red, font-family: helvetica } - * @param {array} allowedDeclarationsList - List of declarations which pass whitelisting. - * @param {object} attributeObject - Object representing the current css property. - * @property {string} attributeObject.type - Typically 'declaration'. - * @property {string} attributeObject.prop - The CSS property, i.e 'color'. - * @property {string} attributeObject.value - The corresponding value to the css property, i.e 'red'. - * @return {function} - When used in Array.reduce, will return an array of Declaration objects - */ - function filterDeclarations(selectedRule) { - return function (allowedDeclarationsList, attributeObject) { - // If this property is whitelisted... - if (selectedRule.hasOwnProperty(attributeObject.prop)) { - var matchesRegex = selectedRule[attributeObject.prop].some(function(regularExpression) { - return regularExpression.test(attributeObject.value); - }); - - if (matchesRegex) { - allowedDeclarationsList.push(attributeObject); - } - } - return allowedDeclarationsList; - }; + return allowedStyles[selector] || allowedStyles['*'] || {}; } function filterClasses(classes, allowed) {