はじめに
こんにちは。アメリカ在住で独学エンジニアを目指している Taira です。
SPA 開発でよく話題になるセキュリティリスクのひとつが XSS(クロスサイトスクリプティング) です。
React はデフォルトでユーザー入力をエスケープしてくれるので一見安心ですが、油断すると簡単に XSS が入り込みます。
この記事では、React でやりがちな XSS 脆弱性のあるコード例と、その回避方法をまとめます。
XSS とは
よく聞くXSSですがそこまで複雑に考える必要はありません。
XSS とは HTML に<script>
などのようなJavaScript をいれることで認証情報(トークン、Cookie)の窃取などを行われてしまいます。
React が安全な理由と危険なパターン
- ✅
{value}
として描画 → React が自動でエスケープしてくれるので安全 - ❌
dangerouslySetInnerHTML
やinnerHTML
直書き → エスケープされないので危険
つまり、React のエスケープ機構をバイパスした瞬間に危険になります。
ありがちな脆弱コード例と修正版
1. dangerouslySetInnerHTML
にユーザー入力を突っ込む
NG
<div dangerouslySetInnerHTML={{ __html: userComment }} />
OK
import DOMPurify from "dompurify";
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(userComment),
}}
/>
2. DOM操作で innerHTML
に代入
NG
useEffect(() => {
ref.current!.innerHTML = userInput; // 危険
}, [userInput]);
OK
import DOMPurify from "dompurify";
useEffect(() => {
ref.current!.innerHTML = DOMPurify.sanitize(userInput);
}, [userInput]);
3. ユーザー入力を href
/src
に直入れ
NG
<a href={userProvidedUrl}>リンク</a>
OK
const url = new URL(userProvidedUrl, window.location.origin);
const safe = ["http:", "https:"].includes(url.protocol) ? url.toString() : "#";
<a href={safe}>リンク</a>
4. MarkdownをそのままHTML化
NG
import { marked } from "marked";
<div dangerouslySetInnerHTML={{ __html: marked(userMarkdown) }} />
OK
import DOMPurify from "dompurify";
import { marked } from "marked";
const html = DOMPurify.sanitize(marked.parse(userMarkdown));
<div dangerouslySetInnerHTML={{ __html: html }} />
script だけじゃない!注意すべき攻撃パターン
XSS といえば <script>alert(1)</script>
を思い浮かべる人が多いですが、実際はもっと多様です。
<img src=x onerror=alert(1)>
<a href="javascript:alert(1)">
<svg onload=alert(1)>
つまり タグや属性を丸ごと削除(サニタイズ)しないと防げないんです。
実戦での対策まとめ
- ユーザー入力は基本 JSX
{value}
で描画(自動エスケープに任せる) - HTML を描画する必要があるなら DOMPurify などでサニタイズ
- URL は
new URL()
でパースし、許可スキームのみ使用 - サーバ側で CSP (Content Security Policy) を設定し、防御を二重化
まとめ
React だからといって XSS に無敵なわけではありません。
「ユーザー入力をそのまま HTML として扱わない」
この原則を徹底するだけでも、大部分の XSS は防げます。