CVE-2022-23458: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Toast UI Grid is a component to display and edit data. Versions prior to 4.21.3 are vulnerable to cross-site scripting attacks when pasting specially crafted content into editable cells. This issue was fixed in version 4.21.3. There are no known workarounds.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2022-09-22T22:15Z
Repository: https://github.com/nhn/tui.grid
Language: JSON
Files changed: README.md, packages/toast-ui.grid/cypress/integration/renderer.spec.ts, packages/toast-ui.grid/package-lock.json, packages/toast-ui.grid/package.json, packages/toast-ui.grid/src/renderer/default.ts, packages/toast-ui.grid/src/view/clipboard.tsx
Commit date: 2022-07-15 10:54:34 +0900
Commit message: fix: cross site script vulnerability (fix #1669) (#1741)

* test: add test for renderer

* fix: cross site script vulnerability

* chore: apply code reviews
Diff: diff --git a/README.md b/README.md
index 0117825..8b8b934 100644
--- a/README.md
+++ b/README.md
@@ -13,10 +13,11 @@
 - [Features](#-features)
 - [Examples](#-examples)
 - [Browser Support](#-browser-support)
 - [Pull Request Steps](#-pull-request-steps)
 - [Contributing](#-contributing)
+- [Dependencies](#-dependencies)
 - [TOAST UI Family](#-toast-ui-family)
 - [Used By](#-used-by)
 - [License](#-license)
 
 ## 📦 Packages
@@ -182,10 +183,14 @@ For more information on PR's steps, please see links in the Contributing section
 - [Code of Conduct](https://github.com/nhn/tui.grid/blob/master/CODE_OF_CONDUCT.md)
 - [Contributing guideline](https://github.com/nhn/tui.grid/blob/master/CONTRIBUTING.md)
 - [Commit convention](https://github.com/nhn/tui.grid/blob/master/docs/COMMIT_MESSAGE_CONVENTION.md)
 - [Issue guideline](https://github.com/nhn/tui.grid/tree/master/.github/ISSUE_TEMPLATE)
 
+## 🔩 Dependencies
+
+* [DOMPurify](https://github.com/cure53/DOMPurify)
+
 ## 🍞 TOAST UI Family
 
 - [TOAST UI Calendar](https://github.com/nhn/tui.calendar)
 - [TOAST UI Chart](https://github.com/nhn/tui.chart)
 - [TOAST UI Editor](https://github.com/nhn/tui.editor)
diff --git a/packages/toast-ui.grid/cypress/integration/renderer.spec.ts b/packages/toast-ui.grid/cypress/integration/renderer.spec.ts
index febc512..96906c6 100644
--- a/packages/toast-ui.grid/cypress/integration/renderer.spec.ts
+++ b/packages/toast-ui.grid/cypress/integration/renderer.spec.ts
@@ -70,5 +70,18 @@ it('should apply the options to default renderer', () => {
     .should('have.class', 'my-class')
     .and('have.css', 'font-weight', '500')
     .should('have.attr', 'myCustom', 'my-custom')
     .should('have.attr', 'title', 'my Lee');
 });
+
+it('should render data to plain text in default renderer', () => {
+  const data = [{ tag: '<img src="" onerror="alert(123)" />' }];
+  const columns = [
+    {
+      name: 'tag',
+    },
+  ];
+
+  cy.createGrid({ data, columns });
+
+  cy.getByCls('cell-content').invoke('html').should('to.eq', '<img src="">');
+});
diff --git a/packages/toast-ui.grid/package-lock.json b/packages/toast-ui.grid/package-lock.json
index 74dd7d8..ccceadb 100644
--- a/packages/toast-ui.grid/package-lock.json
+++ b/packages/toast-ui.grid/package-lock.json
@@ -6123,10 +6123,19 @@
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
       "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
       "dev": true
     },
+    "@types/dompurify": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.3.3.tgz",
+      "integrity": "sha512-nnVQSgRVuZ/843oAfhA25eRSNzUFcBPk/LOiw5gm8mD9/X7CNcbRkQu/OsjCewO8+VIYfPxUnXvPEVGenw14+w==",
+      "dev": true,
+      "requires": {
+        "@types/trusted-types": "*"
+      }
+    },
     "@types/eslint-visitor-keys": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
       "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==",
       "dev": true
@@ -6270,10 +6279,16 @@
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz",
       "integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==",
       "dev": true
     },
+    "@types/trusted-types": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
+      "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
+      "dev": true
+    },
     "@types/uglify-js": {
       "version": "3.9.2",
       "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.9.2.tgz",
       "integrity": "sha512-d6dIfpPbF+8B7WiCi2ELY7m0w1joD8cRW4ms88Emdb2w062NeEpbNCeWwVCgzLRpVG+5e74VFSg4rgJ2xXjEiQ==",
       "dev": true,
@@ -10536,10 +10551,15 @@
       "dev": true,
       "requires": {
         "domelementtype": "1"
       }
     },
+    "dompurify": {
+      "version": "2.3.9",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.9.tgz",
+      "integrity": "sha512-3zOnuTwup4lPV/GfGS6UzG4ub9nhSYagR/5tB3AvDEwqyy5dtyCM2dVjwGDCnrPerXifBKTYh/UWCGKK7ydhhw=="
+    },
     "domutils": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
       "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
       "dev": true,
diff --git a/packages/toast-ui.grid/package.json b/packages/toast-ui.grid/package.json
index 55ab343..d6e1136 100644
--- a/packages/toast-ui.grid/package.json
+++ b/packages/toast-ui.grid/package.json
@@ -48,10 +48,11 @@
     "@cypress/webpack-preprocessor": "^4.0.3",
     "@storybook/addon-knobs": "^5.3.19",
     "@storybook/addon-notes": "^5.3.19",
     "@storybook/html": "^5.3.19",
     "@toast-ui/select-box": "^1.0.0",
+    "@types/dompurify": "^2.3.3",
     "@types/node": "^12.0.0",
     "@types/webpack-env": "^1.13.8",
     "@typescript-eslint/eslint-plugin": "^2.9.0",
     "@typescript-eslint/parser": "^2.9.0",
     "css-loader": "^2.1.1",
@@ -79,10 +80,11 @@
     "webpack-cli": "^3.3.12",
     "webpack-dev-server": "^3.11.0",
     "webpack-merge": "^4.2.1"
   },
   "dependencies": {
+    "dompurify": "^2.3.9",
     "tui-date-picker": "^4.1.0",
     "tui-pagination": "^3.4.0",
     "xlsx": "^0.17.1"
   }
 }
diff --git a/packages/toast-ui.grid/src/renderer/default.ts b/packages/toast-ui.grid/src/renderer/default.ts
index 965d4b2..e5510cb 100644
--- a/packages/toast-ui.grid/src/renderer/default.ts
+++ b/packages/toast-ui.grid/src/renderer/default.ts
@@ -1,8 +1,9 @@
 import { CellRenderer, CellRendererProps } from '@t/renderer';
 import { cls } from '../helper/dom';
 import { isFunction } from '../helper/common';
+import { sanitize } from 'dompurify';
 
 type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 0 : 1) extends <T>() => T extends Y
   ? 0
   : 1
   ? A
@@ -67,8 +68,8 @@ export class DefaultRenderer implements CellRenderer {
   public getElement() {
     return this.el;
   }
 
   public render(props: CellRendererProps) {
-    this.el.innerHTML = `${props.formattedValue}`;
+    this.el.innerHTML = sanitize(`${props.formattedValue}`);
   }
 }
diff --git a/packages/toast-ui.grid/src/view/clipboard.tsx b/packages/toast-ui.grid/src/view/clipboard.tsx
index 73d8ce4..cc1f063 100644
--- a/packages/toast-ui.grid/src/view/clipboard.tsx
+++ b/packages/toast-ui.grid/src/view/clipboard.tsx
@@ -13,10 +13,11 @@ import { KeyboardEventCommandType, KeyboardEventType, keyEventGenerate } from '.
 import { isEdge, isMobile } from '../helper/browser';
 import { getText } from '../query/clipboard';
 import { convertTextToData } from '../helper/common';
 import GridEvent from '../event/gridEvent';
 import { getEventBus, EventBus } from '../event/eventBus';
+import { sanitize } from 'dompurify';
 
 interface StoreProps {
   navigating: boolean;
   editing: boolean;
   rowKey: RowKey | null;
@@ -109,11 +110,12 @@ class ClipboardComp extends Component<Props> {
     if (!this.el) {
       return;
     }
 
     const { el } = this;
-    const html = clipboardData.getData('text/html');
+    const html = sanitize(clipboardData.getData('text/html'));
+
     let data;
     if (html && html.indexOf('table') !== -1) {
       // step 1: Append copied data on contenteditable element to parsing correctly table data.
       el.innerHTML = html;
       // step 2: Make grid data from cell data of appended table element.

Vulnerable code:     this.el.innerHTML = `${props.formattedValue}`;
    const html = clipboardData.getData('text/html');
Fixed code: - [Dependencies](#-dependencies)
## 🔩 Dependencies

* [DOMPurify](https://github.com/cure53/DOMPurify)


it('should render data to plain text in default renderer', () => {
  const data = [{ tag: '<img src="" onerror="alert(123)" />' }];
  const columns = [
    {
      name: 'tag',
    },
  ];

  cy.createGrid({ data, columns });

  cy.getByCls('cell-content').invoke('html').should('to.eq', '<img src="">');
});
    "@types/dompurify": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.3.3.tgz",
      "integrity": "sha512-nnVQSgRVuZ/843oAfhA25eRSNzUFcBPk/LOiw5gm8mD9/X7CNcbRkQu/OsjCewO8+VIYfPxUnXvPEVGenw14+w==",
      "dev": true,
      "requires": {
        "@types/trusted-types": "*"
      }
    },
    "@types/trusted-types": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
      "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
      "dev": true
    },
    "dompurify": {
      "version": "2.3.9",
      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.9.tgz",
      "integrity": "sha512-3zOnuTwup4lPV/GfGS6UzG4ub9nhSYagR/5tB3AvDEwqyy5dtyCM2dVjwGDCnrPerXifBKTYh/UWCGKK7ydhhw=="
    },
    "@types/dompurify": "^2.3.3",
    "dompurify": "^2.3.9",
import { sanitize } from 'dompurify';
    this.el.innerHTML = sanitize(`${props.formattedValue}`);
import { sanitize } from 'dompurify';
    const html = sanitize(clipboardData.getData('text/html'));