From ebaac2c3c45681fc066b894adc9080ef2b3dbd25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 27 Apr 2020 10:03:11 -0400 Subject: [PATCH 1/7] sanitize id + new css prop test --- src/core/Stylesheet.ts | 2 ++ .../components/ControlledTable/index.tsx | 4 ++-- tests/visual/percy-storybook/Css.percy.tsx | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/visual/percy-storybook/Css.percy.tsx diff --git a/src/core/Stylesheet.ts b/src/core/Stylesheet.ts index 85a899f94..f4a976f29 100644 --- a/src/core/Stylesheet.ts +++ b/src/core/Stylesheet.ts @@ -5,6 +5,8 @@ interface IRule { selectorText: string; } +export const sanitizeId = (id: string) => id.replace(/([`~\!\@#\$%\^&\*\(\)=\+\\|\/,.'\";:\?><\[\]\{\}\s])/g, '\\$1'); + class StylesheetFacade { constructor(private readonly name: string) { diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index 85346c645..9807b38c2 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import * as R from 'ramda'; -import Stylesheet from 'core/Stylesheet'; +import Stylesheet, { sanitizeId } from 'core/Stylesheet'; import { KEY_CODES, isCtrlMetaKey, @@ -53,7 +53,7 @@ const MAX_WIDTH_ITERATIONS = 30; export default class ControlledTable extends PureComponent { private readonly menuRef = React.createRef(); - private readonly stylesheet: Stylesheet = new Stylesheet(`#${this.props.id}`); + private readonly stylesheet: Stylesheet = new Stylesheet(`#${sanitizeId(this.props.id)}`); private readonly tableFn = derivedTable(() => this.props); private readonly tableFragments = derivedTableFragments(); private readonly tableStyle = derivedTableStyle(); diff --git a/tests/visual/percy-storybook/Css.percy.tsx b/tests/visual/percy-storybook/Css.percy.tsx new file mode 100644 index 000000000..5e7c4cc74 --- /dev/null +++ b/tests/visual/percy-storybook/Css.percy.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import DataTable from 'dash-table/dash/DataTable'; + +const setProps = () => { }; + +storiesOf('DashTable/CSS override', module) + .add('css with escaped id', () => ({}'} + data={[ + { a: 1, b: 2, c: 3 }, + { a: 2, b: 4, c: 6 }, + { a: 3, b: 6, c: 9 } + ]} + columns={[ + { id: 'a', name: 'A' }, + { id: 'b', name: 'B' }, + { id: 'c', name: 'C' } + ]} + css={[ + { selector: 'td', rule: 'background-color: red !important;' } + ]} + />)) \ No newline at end of file From 7e5ffca5e82b6741038b0cb67f6767b3d274caf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 27 Apr 2020 10:05:02 -0400 Subject: [PATCH 2/7] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fb424ef1..dbaec3e13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - [#724](https://github.com/plotly/dash-table/pull/724) Fix `active_cell` docstring: clarify optional nature of the `row_id` nested prop - [#732](https://github.com/plotly/dash-table/pull/732) Fix a bug where opening a dropdown scrolled the table down its last row - [#731](https://github.com/plotly/dash-table/pull/731) Fix a bug where `data=None` and `columns=None` caused the table to throw an error +- [#766](https://github.com/plotly/dash-table/pull/766) Sanitize table `id` for stylesheet injection (fixes usage with Pattern-Matching callbacks) ## Changed - [#758](https://github.com/plotly/dash-table/pull/758) Improve error message for invalid filter queries From 7463d9bfee6b1829113079b838a0455f44c73c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 27 Apr 2020 14:22:12 -0400 Subject: [PATCH 3/7] Use CSS.escape polyfill + additional common cases tests --- package-lock.json | 8 +++- package.json | 1 + src/core/Stylesheet.ts | 2 - .../components/ControlledTable/index.tsx | 4 +- src/dash-table/index.ts | 2 + tests/visual/percy-storybook/Css.percy.tsx | 48 ++++++++++++------- 6 files changed, 43 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 171f57624..f873d802a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "dash-table", - "version": "4.6.1", + "version": "4.6.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -8946,6 +8946,12 @@ "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", "dev": true }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", + "dev": true + }, "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", diff --git a/package.json b/package.json index 24cfcc750..dde4461f0 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@types/react-select": "^3.0.10", "babel-loader": "^8.0.6", "css-loader": "^3.4.2", + "css.escape": "^1.5.1", "cypress": "^3.8.1", "d3-format": "^1.4.3", "es-check": "^5.1.0", diff --git a/src/core/Stylesheet.ts b/src/core/Stylesheet.ts index f4a976f29..85a899f94 100644 --- a/src/core/Stylesheet.ts +++ b/src/core/Stylesheet.ts @@ -5,8 +5,6 @@ interface IRule { selectorText: string; } -export const sanitizeId = (id: string) => id.replace(/([`~\!\@#\$%\^&\*\(\)=\+\\|\/,.'\";:\?><\[\]\{\}\s])/g, '\\$1'); - class StylesheetFacade { constructor(private readonly name: string) { diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index a5d3746fc..c6a179cab 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import * as R from 'ramda'; -import Stylesheet, { sanitizeId } from 'core/Stylesheet'; +import Stylesheet from 'core/Stylesheet'; import { KEY_CODES, isCtrlMetaKey, @@ -53,7 +53,7 @@ const MAX_WIDTH_ITERATIONS = 30; export default class ControlledTable extends PureComponent { private readonly menuRef = React.createRef(); - private readonly stylesheet: Stylesheet = new Stylesheet(`#${sanitizeId(this.props.id)}`); + private readonly stylesheet: Stylesheet = new Stylesheet(`#${CSS.escape(this.props.id)}`); private readonly tableFn = derivedTable(() => this.props); private readonly tableFragments = derivedTableFragments(); private readonly tableStyle = derivedTableStyle(); diff --git a/src/dash-table/index.ts b/src/dash-table/index.ts index 1fd0452ae..e38d50bf3 100644 --- a/src/dash-table/index.ts +++ b/src/dash-table/index.ts @@ -1,3 +1,5 @@ +import 'css.escape'; // polyfill + import Environment from 'core/environment'; import Logger from 'core/Logger'; diff --git a/tests/visual/percy-storybook/Css.percy.tsx b/tests/visual/percy-storybook/Css.percy.tsx index 5e7c4cc74..3dd88dad9 100644 --- a/tests/visual/percy-storybook/Css.percy.tsx +++ b/tests/visual/percy-storybook/Css.percy.tsx @@ -2,23 +2,37 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import DataTable from 'dash-table/dash/DataTable'; -const setProps = () => { }; +const props = { + setProps: () => { }, + data: [ + { a: 1, b: 2, c: 3 }, + { a: 2, b: 4, c: 6 }, + { a: 3, b: 6, c: 9 } + ], + columns: [ + { id: 'a', name: 'A' }, + { id: 'b', name: 'B' }, + { id: 'c', name: 'C' } + ], + css: [ + { selector: 'td', rule: 'background-color: red !important;' } + ] +}; storiesOf('DashTable/CSS override', module) - .add('css with escaped id', () => ( ()) + .add('leading number', () => ()) + .add('escaped characters', () => ({}'} - data={[ - { a: 1, b: 2, c: 3 }, - { a: 2, b: 4, c: 6 }, - { a: 3, b: 6, c: 9 } - ]} - columns={[ - { id: 'a', name: 'A' }, - { id: 'b', name: 'B' }, - { id: 'c', name: 'C' } - ]} - css={[ - { selector: 'td', rule: 'background-color: red !important;' } - ]} - />)) \ No newline at end of file + />)) + .add('stringified object as id', () => ()); \ No newline at end of file From 524e5b3747c02f4a70053b5b8aea04b3e965463a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 4 May 2020 14:57:34 -0400 Subject: [PATCH 4/7] invalidate cache --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index eb4ec7c5c..42936f263 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,12 +9,12 @@ jobs: steps: - checkout - restore_cache: - key: dep-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} + key: dep1-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} - run: name: Install npm packages command: npm ci - save_cache: - key: dep-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} + key: dep1-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} paths: - node_modules From f35a3b3766799c9a75a9e9cf61081541e284d806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 4 May 2020 15:22:48 -0400 Subject: [PATCH 5/7] renderer needs `npm ci` --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 42936f263..c92353563 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,12 +9,12 @@ jobs: steps: - checkout - restore_cache: - key: dep1-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} + key: dep-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} - run: name: Install npm packages command: npm ci - save_cache: - key: dep1-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} + key: dep-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} paths: - node_modules @@ -27,7 +27,7 @@ jobs: pip install -r dev-requirements.txt --quiet git clone --depth 1 git@github.com:plotly/dash.git dash-main pip install -e ./dash-main[dev,testing] --quiet - cd dash-main/dash-renderer && npm run build && pip install -e . && cd ./../.. + cd dash-main/dash-renderer && npm ci && npm run build && pip install -e . && cd ./../.. - run: name: Build From 76916b9e29ac1b5a57393ecedfb6e39f5777c9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 4 May 2020 15:23:44 -0400 Subject: [PATCH 6/7] npm ci --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c92353563..f01bb27a0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -215,7 +215,7 @@ jobs: . venv/bin/activate git clone --depth 1 git@github.com:plotly/dash.git dash-main pip install -e ./dash-main[dev,testing] --quiet - cd dash-main/dash-renderer && npm run build && pip install -e . && cd ../.. + cd dash-main/dash-renderer && npm ci && npm run build && pip install -e . && cd ../.. - run: name: Install test requirements From 41981cbe86bb2006e7d14d3e363184f9a617154e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Rivet?= Date: Mon, 4 May 2020 22:33:20 -0400 Subject: [PATCH 7/7] Update Css.percy.tsx --- tests/visual/percy-storybook/Css.percy.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/visual/percy-storybook/Css.percy.tsx b/tests/visual/percy-storybook/Css.percy.tsx index 3dd88dad9..31d7ae9d3 100644 --- a/tests/visual/percy-storybook/Css.percy.tsx +++ b/tests/visual/percy-storybook/Css.percy.tsx @@ -34,5 +34,5 @@ storiesOf('DashTable/CSS override', module) />)) .add('stringified object as id', () => ()); \ No newline at end of file + id={"{ id: 3, group: 'A' }"} + />));