3秒でわかる
利用者のブラウザ上で攻撃者のスクリプトを実行させてしまう脆弱性。入力値をそのままHTMLに差し込むと起き、クッキー窃取や偽の入力画面表示につながります。
30秒図解
もう少し詳しく
どういうものか
XSS はクロスサイトスクリプティングの略で、攻撃者が用意した JavaScript を Web ページに紛れ込ませ、そのページを開いた利用者のブラウザで実行させる攻撃です。原因はほぼ一つで、利用者から受け取った文字列を検証も変換もせず HTML として出力していることにあります。
混入の経路で三つに分けられます。URL のクエリがそのまま画面に出る反射型、投稿内容がデータベースに保存されて後から全員に配られる格納型、サーバーを経由せずブラウザ内の JavaScript が危険な代入をする DOM 型です。掲示板やレビュー欄のように他人の入力を表示する画面は、格納型の標的になりやすい場所です。
なぜ必要か
スクリプトが利用者のブラウザで動くと、そのページと同じ権限を持ちます。ログイン中のクッキーを外部へ送る、画面に偽のログインフォームを描いてパスワードを入力させる、本人になりすまして退会や送金の操作を投げる、といったことが本人の操作と区別できない形で起こります。サーバーが無傷でも被害は利用者側で完結してしまうため、出力する側で防ぐしかありません。
具体例
危険な書き方と安全な書き方の差は一行です。
const comment = new URLSearchParams(location.search).get("c");
// 危険。文字列が HTML として解釈される
document.getElementById("out").innerHTML = comment;
// 安全。文字列は文字として表示される
document.getElementById("out").textContent = comment;サーバー側のテンプレートでも同じで、Express と EJS なら二重括弧の記法が自動エスケープ、三重括弧が生 HTML 出力にあたります。生 HTML を出す構文を使った瞬間、そこは自分でエスケープする責任を負う場所になります。
つまずきやすいところ
山括弧を実体参照へ置き換えれば終わりだと考えると抜けます。属性値の中に差し込む場合は引用符も対象ですし、リンク先の href に利用者の文字列を入れるなら http と https で始まるものだけを通す検査が別に要ります。スクリプトタグの内側やイベントハンドラ属性の中は、HTML のエスケープでは守れません。
もう一つは、入力時にまとめて無害化しようとする発想です。同じ文字列がメールやログや CSV にも回るため、無害化は出力する直前に、出力先に合わせて行うのが原則です。
似た用語との違い
| 用語 | 攻撃される場所 | 主な防御 |
|---|---|---|
| XSS | 利用者のブラウザ | 出力時のエスケープ、CSP |
| CSRF | ログイン中の利用者の権限 | トークン照合、SameSite 属性 |
| SQL インジェクション | サーバーのデータベース | プレースホルダによる問い合わせ |
覚え方
「文字列を HTML として渡した場所が穴になる」と覚えると判断が速くなります。innerHTML を書こうとした手が止まるようになれば十分です。