ReactやNext.jsを使っていれば、JSXが自動的にエスケープしてくれるからXSSは起きない──そう信じている開発者は多い。確かにReactのJSXは文字列をレンダリングする際にHTMLエスケープを行うが、それだけでは防げないXSSの入口が少なくとも7つ存在する。
1. dangerouslySetInnerHTML
最も有名な穴。名前に "dangerously" と入っているにもかかわらず、CMSから取得したHTMLをそのまま表示するために使われているケースが多い。
// 危険: サニタイズなしで挿入
<div dangerouslySetInnerHTML={{ __html: userContent }} />
// 対策: DOMPurifyでサニタイズしてから挿入
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />
DOMPurifyは現時点で最も信頼性の高いHTMLサニタイザーライブラリだ。sanitize-html など他の選択肢もあるが、バイパスされた実績が少ない点でDOMPurifyが推奨される。
2. href属性へのjavascript:スキーム
ReactはJSXの属性値をエスケープするが、href 属性に javascript: スキームのURLを渡した場合は防げない。
// 危険: ユーザー入力をそのままhrefに渡す
<a href={userUrl}>リンク</a>
// userUrl = "javascript:alert(document.cookie)" でXSS成立
// 対策: プロトコルをホワイトリストで検証
function sanitizeUrl(url: string): string {
const parsed = new URL(url, 'https://dummy.com');
if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
return '#';
}
return url;
}
3. Server Componentsでの直接HTML出力
Next.js App Routerでは、Server Componentsの中でHTMLを直接返す処理が書ける。この場合もエスケープ漏れに注意が必要だ。
// Server Component内で動的にmetaタグを生成する場合
// OGP画像のURLにユーザー入力が含まれると危険
export async function generateMetadata({ params }) {
const post = await getPost(params.id);
return {
openGraph: {
// post.titleにスクリプトが含まれていても
// Next.jsのmetadata APIが適切にエスケープする
title: post.title,
},
};
}
Next.jsのMetadata APIを使えば基本的に安全だが、手動で <head> にタグを挿入する場合は話が変わる。
4. SVGファイルのインライン読み込み
SVGはXMLベースのフォーマットであり、<script> タグを含めることができる。ユーザーがアップロードしたSVGをそのままインライン表示すると、XSSが成立する。
<!-- 悪意のあるSVG -->
<svg xmlns="http://www.w3.org/2000/svg">
<script>alert(document.cookie)</script>
</svg>
対策は、SVGファイルを <img> タグで読み込むか、サーバーサイドでSVGのサニタイズを行うことだ。<img> タグ経由であればスクリプトは実行されない。
5. CSS-in-JSでのスタイルインジェクション
styled-componentsやEmotionなどのCSS-in-JSライブラリで、ユーザー入力をスタイルに埋め込むケースも危険だ。
// 危険: ユーザー入力をCSSに直接埋め込む
const UserDiv = styled.div`
background-color: ${props => props.userColor};
`;
// userColor = "red; } body { background: url('javascript:...');" のような入力
CSSインジェクション自体でJavaScriptを実行するのは現代のブラウザでは困難だが、url() を使った外部リクエストでデータを窃取する手法は有効だ。
6. window.locationやwindow.nameの参照
ReactコンポーネントでURL パラメータを直接レンダリングしている場合、Reflected XSSが成立する可能性がある。
// 危険: URLパラメータをサニタイズせずに表示
function SearchResults() {
const params = useSearchParams();
const query = params.get('q');
return <div dangerouslySetInnerHTML={{ __html: `検索結果: ${query}` }} />;
}
これは dangerouslySetInnerHTML との組み合わせで発生するが、検索結果ページなどで意外とよく見かけるパターンだ。
7. サードパーティスクリプトの読み込み
next/script やuseEffectで外部スクリプトを動的に読み込む際、そのURLにユーザー入力が含まれていると、任意のスクリプトを実行される。
// 危険: ユーザー入力からスクリプトURLを構築
<Script src={`https://cdn.example.com/${userInput}/analytics.js`} />
防御の多層化
XSS対策はアプリケーションコードだけでは不完全だ。以下のHTTPヘッダーを組み合わせることで、仮にXSSが発生しても被害を最小限に抑えられる。
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}';
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
特にCSP(Content Security Policy)は強力な第二の防御線になる。unsafe-inline を許可しないだけで、大半のXSS攻撃は無効化される。
ReactやNext.jsの自動エスケープに頼りきるのではなく、「どこが自動エスケープの範囲外なのか」を正確に把握しておくことが重要だ。