XSS脆弱性の基礎からDOMPurifyによる防御までReactで学ぶ
XSS(クロスサイトスクリプティング)とは
XSS(クロスサイトスクリプティング)とは、Webアプリケーションに悪意のあるスクリプト(JavaScriptなど)を埋め込み、ユーザーのブラウザ上で実行させることで情報の盗聴や不正操作を行う攻撃手法です。
例えば、ブログの記事投稿機能にscript
タグで悪意あるスクリプト(ポップアップ表示や外部サイトへの自動リダイレクトなど)を埋め込んで投稿するケースなどがあります。
Twitter,Yahooニュース,YouTubeといった著名なサービスでも、
以下の表のように被害を受けたことがあります。
サービス | 西暦 | 概要 | 備考 |
---|---|---|---|
2010 | onMouseOver属性を悪用し、マウスオーバーだけでポップアップ表示やリダイレクトを引き起こすスクリプトが拡散された | ||
Yahoo!ニュース | 2024 | スマホアプリにXSS脆弱性があり、端末内の他アプリを介してWebView上で任意のスクリプトが実行される可能性があった | IPA(独立行政法人情報処理推進機構)から注意喚起がされた。実害に関する情報は発見できず。 |
YouTube | 2010 | コメント欄に埋め込まれたスクリプトにより、ポップアップ表示や悪質なサイトへのリダイレクトが発生 |
ReactにおけるXSS
Reactに関しては自動的にエスケープ処理が施されるので、基本的に普通に開発していればXSSが起きることは考えにくいです。
※エスケープとは、HTMLやJavaScriptなどで特別な意味を持つ文字を、そのまま表示用の文字に変換することで悪意のあるスクリプトを実行されるのを防ぎます。
例えば、以下のように置換されます。
置換前 | 置換後 |
---|---|
< |
< |
> |
> |
& |
& |
" |
" |
' |
' |
ただし、dangerouslySetInnerHTMLを用いる場合は別です。
このメソッドは、引数に渡された文字列をHTMLとして扱う(つまり、エスケープ処理をしない)ので、
公式ドキュメントにも書かれている通り、何も工夫をしないと簡単にXSS脆弱性が発生します。
dangerouslySetInnerHTMLにおけるXSSの防ぎ方
基本的にはdangerouslySetInnerHTMLを使わないことが一番のXSS対策なのですが、社内の人間のみしか入稿できないシステムで、太字や色付き文字などの装飾を許可するなどの自由度の高い表現をしたい場合などに、HTMLでの入稿を許可せざるを得ない場合があると思います。
その場合は、サニタイズさせてから表示させるしかないです。
※サニタイズとエスケープの違いは以下になります。
似ているけれども意味が少し異なります。
項目 | サニタイズ | エスケープ |
---|---|---|
処理内容 | 危険なタグ・属性を削除/無効化 | 特定の記号を文字参照に置換 |
主な用途 | 安全なHTML表示(タグ一部許可) | HTMLで文字列として表示 |
出力形式 | HTML | 文字列 |
処理後の文字列の例 | <p>こんにちは</p> |
<p>こんにちは</p> |
ユースケース |
dangerouslySetInnerHTML で使う |
通常のテンプレート表示(例:React JSX) |
サニタイズの方法
最も簡単な方法はDOMPurifyというライブラリを使うことです。
根拠としては以下です。
- 平均して1〜2ヶ月間隔くらいではリリースが継続的に行われており、陳腐化する可能性が現時点では低い
- Githubのスターを15,600(当記事の執筆時点)で獲得しており、開発者の評価が高く信頼できる
- 上記の事情があるので、ライブラリで対応できない要件が発生したなどの特殊な場合を除き、自力でサニタイズ処理を実装するのは車輪の再発明にあたり時間の無駄
簡単なサンプルコードとしては以下のようになり、
DOMPurify.sanitize()
を実行すれば、スクリプトの実行がされないようにサニタイズしてくれます。
import { useState } from "react";
import DOMPurify from "dompurify";
export default function Home() {
const [input, setInput] = useState<string>(`<p>こんにちは</p>
<script>alert("XSS!")</script>
<img src="dummy" onerror="alert('画像からXSS!')">`);
const [sanitized, setSanitized] = useState<string>("");
const handleSanitize = () => {
const clean = DOMPurify.sanitize(input);
setSanitized(clean);
};
return (
<main style={{ padding: "2rem" }}>
<h1>DOMPurify 動作確認</h1>
<p>
HTMLを入力して「サニタイズ」ボタンを押すと安全なHTMLが表示されます。
</p>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
rows={6}
cols={60}
style={{ display: "block", marginBottom: "1rem" }}
/>
<button onClick={handleSanitize} style={{ marginBottom: "1rem" }}>
サニタイズ
</button>
<h2>サニタイズ後のHTML(表示例)</h2>
<div
style={{ border: "1px solid #ccc", padding: "1rem" }}
dangerouslySetInnerHTML={{ __html: sanitized }}
/>
</main>
);
}
実際の画面の表示としては以下の画像のようになり、alertは表示されません。
また、特定のタグについて許可するなどの細かいカスタマイズの方法についてREADMEに記載がありますが、よほど細かい要求がない限りはカスタマイズせずに済みます。
まとめ
- XSS(クロスサイトスクリプティング) は、ブラウザ上で悪意あるスクリプトを実行させ、情報の盗聴や不正操作を行う攻撃手法。
- Reactは通常、自動エスケープによりXSSを防いでいるが、dangerouslySetInnerHTML 使用時は脆弱性が発生しやすい。
- エスケープは文字参照への置換、サニタイズは危険なタグや属性の削除/無効化と目的が異なる。
- HTMLを安全に表示する必要がある場合は、信頼できるサニタイズライブラリ(例:DOMPurify)を利用するのが効果的。
- 自力でサニタイズ処理を実装するのは車輪の再発明になりやすく、保守性・安全性の面からも避けるべき。
Discussion