【React】DOMPurifyを導入し、dangerouslySetInnerHTMLを使いつつXSSの危険性を低減させる

## はじめに
とあるReactアプリの実装でユーザーが入力したHTMLをレンダリングして表示させる部分がありました。
レンダリングの方法としては`dangerouslySetInnerHTML`がありましたが、調べるうちにこの方法にはセキュリティ上の脆弱性が存在していることがわかりました。

ここでは`dangerouslySetInnerHTML`の危険性と、その問題を回避するために採用した`DOMPurify`について書いていきます。

## dangerouslySetInnerHTMLとXSSの危険性
ReactでHTMLをレンダリングさせる方法としては`dangerouslySetInnerHTML`が挙げられます。

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

`<div dangerouslySetInnerHTML={( __html: (描画したいHTML) )}>`の形で記述することで、レンダリングしたいHTMLをReact上で表示させることができます。

**React公式ドキュメント上のサンプルコード**
```react:sample.jsx
const markup = { __html: '<p>some raw html</p>' };
return <div dangerouslySetInnerHTML={markup} />;
```

ただし、`dangerouslySetInnerHTML`は**HTMLとしてスクリプト入りのコードを記述するとそのスクリプトが実行されてしまうという問題**を抱えています。

これを悪用してユーザー情報を抜き取ったりサイト上で意図せぬ動作を引き起こさせたりすることがいわゆる**XSS＝クロスサイトスクリプティング**です。

参考

https://www.ipa.go.jp/security/vuln/websecurity/cross-site-scripting.html


## 危険なコードの例
※React公式から引用

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

例えば以下のようなコードがあり、HTMLにスクリプトが書かれているとします。
```react
const post = {
  content: `<img src="" onerror='alert("you were hacked")'>`
};

export default function MarkdownPreview() {
  const markup = { __html: post.content };
  return <div dangerouslySetInnerHTML={markup} />;
}
```
この場合、`content`に書かれた`onerror="alert"`の処理が走ってしまうことになります。
上記の事象を応用して悪意のあるスクリプトを埋め込むことでXSSが引き起こされてまいます。

## XSS対策としてのサニタイズとDOMPurify
dangerouslySetInnerHTMLには上記のようにスクリプトがそのまま実行されてしまう脆弱性が存在します。

その対策として必要になってくるのが**サニタイズ**です。

>HTMLや属性値などを出力する際、サニタイズ処理をすることで特別な意味を持つメタ文字を無効化し XSSを防ぐことができます。サニタイズ処理をすることで、例えば<→&lt;、>→&gt;のようにタグを無害な文字列に置き換えることができます。
引用元：https://zenn.dev/oreo2990/articles/d33a264b2d8b4c

上記の処理を行うことでHTMLのレンダリング時にスクリプト部分を除去する必要があります。
Reactでこれを行ってくれるライブラリが**DOMPurify**です。

https://github.com/cure53/DOMPurify

### 導入方法
**①インストール**
`npm install isomorphic-dompurify`でインストールします。

**②インポート**
使いたいファイル内で`import DOMPurify from 'isomorphic-dompurify';`の形でインポートします。

③対象のHTMLをサニタイズする
HTML要素を囲む外側で`DOMPurify.sanitize`を記述します。
`DOMPurify.sanitize("記述したいHTML")`の形です。


### DOMPurifyを使うことの効果
#### dangerouslySetInnerHTML単体のコード
まずはXSS対策が行われていない`dangerouslySetInnerHTML`のコードを見てみます。
```react:no-sanitize.jsx
<div
dangerouslySetInnerHTML={{
  __html: `<img src="" onerror='alert("you were hacked")'>`,
}}
```
するとHTMLに入っているアラートが実行されてしまいます。
![スクリーンショット 2025-05-28 23.39.25 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4021196/338386b8-dae7-445d-8c20-02f55c944335.png)
HTMLを見てみると、`onerror`属性として意図せぬスクリプトが混入していることがわかります。
![スクリーンショット 2025-05-28 23.40.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4021196/31f2e5a5-38e8-4cd3-8770-c3f5001c8c6a.png)

#### DOMPurifyを併用したコード
次にDOMPurifyを導入したコードを見てみます。
```react:sanitize.jsx
<div
  dangerouslySetInnerHTML={{
    __html: DOMPurify.sanitize(
      `<img src="" onerror='alert("you were hacked")'>`,
    ),
  }}
></div>
```
出力されたコードを見てみるとアラートが発生せず、**該当の部分からonerror属性の記述が削除されている**ことがわかりました。そのため悪意のあるスクリプトが実行されずに済んだということです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4021196/b2495d9a-b675-4f7f-830b-682088fb9c48.png)

これらのことから、**DOMPurify**を使うことでXSS対策に一定の効果があると言えそうです。

## 最後に
`dangerouslySetInnerHTML`はHTML文字列をレンダリングさせるためには便利な反面、そのままでは脆弱性のリスクが潜んでいます。

**DOMPurify**などのサニタイズ方法と併用することで脆弱性を低減させてHTMLレンダリングをすることが可能になります。


## 参考資料

https://qiita.com/hiromoon/items/f3ed77abd338139ba97b

https://www.ipa.go.jp/security/vuln/websecurity/cross-site-scripting.html

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

https://qiita.com/Yasushi-Mo/items/c8ab10669772e9c5ab28

https://qiita.com/kensiiwasaki/items/deb6d2ab6ba6b20308b6