目次
Reactで外部から取得したHTMLを表示したいことがあります。マークダウンをHTMLに変換した結果や、CMSから取得したリッチテキストなどです。このときdangerouslySetInnerHTMLを使いますが、名前の通り危険が伴います。DOMPurifyを使ってこの問題を解決したので備忘録です。
XSS脆弱性とは
XSS(クロスサイトスクリプティング)は、Webアプリケーションの脆弱性の一つです。攻撃者が悪意のあるスクリプトを注入し、他のユーザーのブラウザで実行させます。
たとえば、ブログ記事のコメント欄で以下のような入力があった場合を考えます。
<script>alert('XSS攻撃!')</script>
これをそのまま表示すると、スクリプトが実行されてしまいます。攻撃者はこれを悪用して、ログイン中のユーザーのCookieを盗んだり、偽のログインフォームを表示してパスワードを取得したりできます。
dangerouslySetInnerHTMLとは
Reactは通常、文字列をそのままテキストとして表示します。<b>太字</b>という文字列を渡しても、タグがエスケープされてそのまま表示されます。
HTMLとして解釈させたい場合はdangerouslySetInnerHTMLを使います。
// そのまま表示 → <b>太字</b>
<div>{htmlString}</div>
// HTMLとして解釈 → **太字**
<div dangerouslySetInnerHTML={{ __html: htmlString }} />
名前に「dangerously」とあるように、信頼できないHTMLを渡すとXSS脆弱性になります。マークダウンから変換したHTMLやCMSからのコンテンツなど、外部データを表示する場合はサニタイズが必要です。
HTMLサニタイズライブラリの比較
HTMLをサニタイズするライブラリはいくつかあります。
| ライブラリ | サイズ | 特徴 |
|---|---|---|
| DOMPurify | 約10KB | 最も広く使われている。XSS攻撃パターンを網羅 |
| sanitize-html | 約50KB | サーバーサイドでも動作。設定が柔軟 |
| xss | 約30KB | 中国語ドキュメントが充実 |
DOMPurifyを選ぶ理由は以下の通りです。
- 軽量で高速
- セキュリティ専門家によるメンテナンス
- 新しいXSSベクターへの迅速な対応
- TypeScriptの型定義が公式提供
DOMPurifyとは
DOMPurifyは、HTMLをサニタイズ(無害化)するJavaScriptライブラリです。危険なタグや属性を除去し、安全なHTMLだけを残します。
インストール
npmでインストールします。
npm install dompurify
TypeScriptを使う場合は型定義もインストールします。
npm install -D @types/dompurify
基本的な使い方
sanitize メソッドにHTMLを渡すだけです。
import DOMPurify from 'dompurify'
const dirtyHtml = '<script>alert("XSS")</script><p>安全なテキスト</p>'
const cleanHtml = DOMPurify.sanitize(dirtyHtml)
console.log(cleanHtml)
// 出力: <p>安全なテキスト</p>
<script> タグが除去され、安全な <p> タグだけが残ります。
Reactでの実装例
dangerouslySetInnerHTMLとDOMPurifyを組み合わせたコンポーネントを作ります。
import DOMPurify from 'dompurify'
type Props = {
html: string
}
export const SafeHtml = ({ html }: Props) => {
return (
<div
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}
/>
)
}
使い方はシンプルです。
// マークダウンから変換したHTMLを表示
<SafeHtml html={markdownToHtml(content)} />
// CMSから取得したリッチテキストを表示
<SafeHtml html={article.body} />
設定のカスタマイズ
デフォルト設定で多くのケースに対応できますが、必要に応じてカスタマイズも可能です。
許可するタグを制限する
const cleanHtml = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
})
すべての属性を除去する
const cleanHtml = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_ATTR: []
})
テキストのみを抽出する
const textOnly = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: [],
ALLOWED_ATTR: []
})
留意点
DOMPurifyを使っても完全な安全性は保証されません。以下の点に注意してください。
- サーバーサイドでも検証する :クライアントサイドだけでなく、サーバーサイドでも入力を検証してください。
- 最新版を使う :新しいXSS攻撃パターンに対応するため、定期的にアップデートしてください。
- CSPと組み合わせる :Content Security Policy(CSP)を設定し、多層防御を実現してください。
参考資料
DOMPurifyの詳細な設定オプションは公式READMEを参照してください。
XSS脆弱性についてはOWASPのガイドが参考になります。
