【React】DOMPurify で XSS 対策

# はじめに
この記事では、HTML (DOM) の XSS 対策用のライブラリである **DOMPurify** の React での利用方法について記載します。

https://github.com/cure53/DOMPurify

# 開発環境
開発環境は以下の通りです。

- Windows 11
- Chrome 131
- TypeScript 5.5.3
- React 18.3.1
- Vite 5.4.7
- Node.js 20.13.1
- npm 10.8.1
- DOMPurify 3.2.3

# インストール
まずは DOMPurify をインストールします。

```powershell
npm i dompurify
```

# DOMPurify を利用しないケース
比較のため、DOMPurify を利用せずに React の `dangerouslySetInnerHTML` に script タグを渡した時の挙動を確認します。

https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html

```Unsafe.tsx
import { FC } from "react";

export const Unsafe: FC = () => {
  const potentiallyUnsafeHTML = `
    <div>
      <h2>Hello World!</h2>
      <script>alert('This is unsafe!');</script>
    </div>
  `;

  return (
    <div>
      <h2>Without DOMPurify</h2>
      <div dangerouslySetInnerHTML={{ __html: potentiallyUnsafeHTML }} />
    </div>
  );
};

```

Chrome の開発者ツールの Elements タブを確認すると、script タグが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/1fcd9ab7-bfb7-b649-8799-adcfdefff419.png)

# DOMPurify を利用するケース
DOMPurify を利用する際は、`DOMPurify.sanitize` の引数に追加する HTML を渡します。

```Safe.tsx
import { FC } from "react";
import DOMPurify from "dompurify";

export const Safe: FC = () => {
  const potentiallyUnsafeHTML = `
    <div>
      <h2>Hello World!</h2>
      <script>alert("This is unsafe!");</script>
    </div>
  `;

  const sanitizedHTML = DOMPurify.sanitize(potentiallyUnsafeHTML);

  return (
    <div>
      <h2>With DOMPurify</h2>
      <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
    </div>
  );
};

```

DOMPurify を利用すると、script タグは表示されません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/da407a6e-d6bd-872e-60dc-d563f70eba39.png)

:::note
画面表示時だけでなく、画面操作時の script も同様に無効化できます。

- DOMPurify を利用しないケース

```Unsafe.tsx
import { FC } from "react";

export const Unsafe: FC = () => {
  const potentiallyUnsafeHTML = `
    <div>
      <button onclick="alert('Still unsafe!')">Click me</button>
    </div>
  `;

  return (
    <div>
      <h2>Without DOMPurify</h2>
      <div dangerouslySetInnerHTML={{ __html: potentiallyUnsafeHTML }} />
    </div>
  );
};

```

DOMPurify を利用しないと script が実行されます。

![Vite-React-TS-Google-Chrome-2024-12-25-09-23-45.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/c03643c0-32ee-ecc3-9478-c578d83aa9e1.gif)


- DOMPurify を利用するケース

```Safe.tsx
import { FC } from "react";
import DOMPurify from "dompurify";

export const Safe: FC = () => {
  const potentiallyUnsafeHTML = `
    <div>
      <button onclick="alert('Still unsafe!')">Click me</button>
    </div>
  `;

  const sanitizedHTML = DOMPurify.sanitize(potentiallyUnsafeHTML);

  return (
    <div>
      <h2>With DOMPurify</h2>
      <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
    </div>
  );
};

```

DOMPurify を利用すると script は実行されません。

![Vite-React-TS-Google-Chrome-2024-12-25-09-23-25_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/9c231895-0ff5-fc6b-e907-20791a9b92de.gif)

:::

:::note
タブナビング攻撃などの対象になる脆弱性を持つ `target=”_blank”` も無効化してくれます。

- DOMPurify を利用しないケース

```Unsafe.tsx
import { FC } from "react";

export const Unsafe: FC = () => {
  const potentiallyUnsafeHTML = `
    <div>
      <a href="https://example.com" target="_blank">
        Visit Example
      </a>
    </div>
  `;

  return (
    <div>
      <h2>Without DOMPurify</h2>
      <div dangerouslySetInnerHTML={{ __html: potentiallyUnsafeHTML }} />
    </div>
  );
};

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/7ca1bfa7-e71c-2f86-9f32-296b541e78d5.png)


- DOMPurify を利用するケース

```Safe.tsx
import { FC } from "react";
import DOMPurify from "dompurify";

export const Safe: FC = () => {
  const potentiallyUnsafeHTML = `
    <div>
      <a href="https://example.com" target="_blank">
        Visit Example
      </a>
    </div>
  `;

  const sanitizedHTML = DOMPurify.sanitize(potentiallyUnsafeHTML);

  return (
    <div>
      <h2>With DOMPurify</h2>
      <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
    </div>
  );
};
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/9e084ad8-04e8-831c-13b2-3f492a422592.png)

:::

# 参考
- [DOMPurify](https://github.com/cure53/DOMPurify)