diff --git a/CHANGELOG.md b/CHANGELOG.md index 29dc47aba7..7128b867f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Fix Avatar alignment issue and initials for long names @mnajdova ([#38](https://github.com/stardust-ui/react/pull/38)) - Changing the default styles for Input component @alinais ([#25](https://github.com/stardust-ui/react/pull/25)) - Upgrade Typescript to version 3.0.1 @luzhon ([#67](https://github.com/stardust-ui/react/pull/67)) +- Prevent Fela from rendering CSS property values that could crash all styling on the page @kuzhelov ([#65](https://github.com/stardust-ui/react/pull/65)) ### Features - Behaviors for accessibility roles and other ARIA attributes @smykhailov, @jurokapsiar, @sophieH29 ([#29](https://github.com/stardust-ui/react/pull/29)) diff --git a/src/lib/felaRenderer.tsx b/src/lib/felaRenderer.tsx index 1e8727acd3..c5a4cf5b0d 100644 --- a/src/lib/felaRenderer.tsx +++ b/src/lib/felaRenderer.tsx @@ -1,4 +1,5 @@ import { createRenderer } from 'fela' +import felaSanitizeCss from './felaSanitizeCssPlugin' import felaPluginFallbackValue from 'fela-plugin-fallback-value' import felaPluginPlaceholderPrefixer from 'fela-plugin-placeholder-prefixer' import felaPluginPrefixer from 'fela-plugin-prefixer' @@ -6,8 +7,15 @@ import rtl from 'fela-plugin-rtl' const createRendererConfig = (options: any = {}) => ({ plugins: [ + // is necessary to prevent accidental style typos + // from breaking ALL the styles on the page + felaSanitizeCss({ + skip: ['content'], + }), + felaPluginPlaceholderPrefixer(), felaPluginPrefixer(), + // Heads up! // This is required after fela-plugin-prefixer to resolve the array of fallback values prefixer produces. felaPluginFallbackValue(), diff --git a/src/lib/felaSanitizeCssPlugin.ts b/src/lib/felaSanitizeCssPlugin.ts new file mode 100644 index 0000000000..0dd9f2b005 --- /dev/null +++ b/src/lib/felaSanitizeCssPlugin.ts @@ -0,0 +1,63 @@ +/** + * Checks whether provided CSS property value is safe for being rendered by Fela engine. + */ +const isValidCssValue = (value: any) => { + if (typeof value !== 'string') { + return true + } + + const openingBrackets = '({[' + const closingBrackets = ')}]' + + const openingBracketsStack = [] + + /** + * This loop logic checks whether braces sequence of input argument is valid. + * Essentially, it ensures that each of the '(', '{', '[' braces + * - is properly matched by its complementary closing character + * - closing brace properly corresponds to the last opened one + */ + for (let i = 0; i < value.length; ++i) { + const currentCharacter = value[i] + if (openingBrackets.includes(currentCharacter)) { + openingBracketsStack.push(currentCharacter) + } else if (closingBrackets.includes(currentCharacter)) { + const lastOpeningBracket = openingBracketsStack.pop() + if ( + openingBrackets.indexOf(lastOpeningBracket) !== closingBrackets.indexOf(currentCharacter) + ) { + return false + } + } + } + + return openingBracketsStack.length === 0 +} + +export default (config?: { skip?: string[] }) => { + const cssPropertiesToSkip = [...((config && config.skip) || [])] + + const sanitizeCssStyleObject = styles => { + const processedStyles = {} + + Object.keys(styles).forEach(cssPropertyName => { + const cssPropertyValue = styles[cssPropertyName] + + if (typeof cssPropertyValue === 'object') { + processedStyles[cssPropertyName] = sanitizeCssStyleObject(cssPropertyValue) + return + } + + const isPropertyToSkip = cssPropertiesToSkip.some( + propToExclude => propToExclude === cssPropertyName, + ) + if (isPropertyToSkip || isValidCssValue(cssPropertyValue)) { + processedStyles[cssPropertyName] = cssPropertyValue + } + }) + + return processedStyles + } + + return sanitizeCssStyleObject +} diff --git a/test/specs/lib/felaSanitizeCssPlugin-test.ts b/test/specs/lib/felaSanitizeCssPlugin-test.ts new file mode 100644 index 0000000000..f329142b4e --- /dev/null +++ b/test/specs/lib/felaSanitizeCssPlugin-test.ts @@ -0,0 +1,65 @@ +import sanitizeCss from 'src/lib/felaSanitizeCssPlugin' + +const assertCssPropertyValue = (value: string, isValid: boolean) => { + test(`assert that '${value}' is ${isValid ? 'valid' : 'invalid'}`, () => { + const sanitize = sanitizeCss() + + const style = { display: value } + const sanitizedStyle = sanitize(style) + + expect(sanitizedStyle).toEqual(isValid ? style : {}) + }) +} + +const sanitize = sanitizeCss() + +describe('felaSanitizeCssPlugin', () => { + test('should ensure there are no non-closed brackets in CSS property value', () => { + const style = { + display: 'block', + backgroundImage: 'url(../../', + } + + expect(sanitize(style)).toEqual({ display: 'block' }) + }) + + test('should skip numeric CSS property values', () => { + expect(sanitize({ top: 0 })).toEqual({ top: 0 }) + }) + + test('should recursively process nested objects', () => { + const style = { + display: 'inline', + '::before': { + color: 'rgba(', + }, + } + + expect(sanitize(style)).toEqual({ + display: 'inline', + '::before': {}, + }) + }) + + test('should skip excluded CSS props', () => { + const sanitize = sanitizeCss({ + skip: ['propertyWithInvalidValue'], + }) + + const style = { + display: 'block', + margin: '0 0 0 0', + propertyWithInvalidValue: 'rgba(', + } + + expect(sanitize(style)).toEqual(style) + }) + + describe('should properly filter invalid bracket sequences', () => { + assertCssPropertyValue('rgba(', false) + assertCssPropertyValue('rgba(0,0', false) + assertCssPropertyValue('rgba(0,0}', false) + + assertCssPropertyValue(`url('../../lib')`, true) + }) +})