XSSの流れと基本的な対策
要点:XSSとは「信頼されない入力がHTML/JS/CSSの文脈に混入し、ブラウザで任意のJavaScriptが実行される」脆弱性である。
1. なぜXSSが起きるのか(概要)
- ブラウザは
<タグ>
、"属性"
、javascript:
など文脈依存でデータを解釈する。未エスケープの<
>
"
'
や危険なURIを埋め込むと文脈が切り替わり、スクリプト実行につながる。 - 入力バリデーションは補助。決定打は「出力時の文脈エスケープ」と安全な組み立てAPIの使用。
- 文字コード誤判定(例: UTF-8以外に誤認)やDOM操作(
innerHTML
)も誘因。
2. 攻撃者の主目的
- セッション・認可の悪用:セッションID窃取、ユーザー権限での操作(送金・設定変更・データ閲覧)。
- 情報収集:DOM上の個人情報、ローカル/セッションストレージ、非HTTPOnly Cookie、IndexedDB等。
- フィッシング/画面改ざん:偽フォーム・モーダル、キー入力の盗み見。
3. XSSの基本的な流れ(ダイアグラム)
[攻撃者入力]
│ (URL/フォーム/コメント)
▼
[サーバ側処理]
├─(反射) レスポンスへ直埋め
└─(保存) DB等に保存→後配信
▼
[ブラウザ描画]
├─ HTML本文/属性/URL/JS文字列/CSS
└─ 不適切な組み立て・未エスケープ
▼
[JS実行]
├─ Cookie/Storage/DOM取得
├─ 同一オリジンAPI呼び出し
└─ 画面改ざん・操作乗っ取り
4. 反射型XSSと持続型(ストア型)XSS
4.1 定義と特徴(比較表)
項目 | 反射型XSS | 持続型(ストア型)XSS |
---|---|---|
攻撃コードの保存 | 保存されない(要求→応答で反射) | サーバ/DBに保存され続ける |
被害条件 | 被害者が細工URL等を踏む | 対象ページを開くだけで発動 |
拡散性 | 低め(誘導が要る) | 高い(閲覧者全員影響) |
典型箇所 | 検索結果、エラーメッセージ | コメント欄、プロフィール、掲示板 |
主な対策 | 文脈エスケープ、URL検証、CSP | 文脈エスケープ、サニタイズ、CSP |
4.2 簡易例
- 反射型:
/search?q=<script>alert(1)</script>
をそのまま結果画面に出力。 - 持続型: コメントとして
<img src=x onerror=alert(1)>
を保存し、閲覧者のブラウザで実行。
補足:DOM型XSSはサーバ往復なしで、フロントJSが
innerHTML
等で不正なDOMを生成して発生。対策は「危険なDOM APIを避ける」こと。
5. 基本的な対策3つ(必須の土台)
5.1 HTML要素内容のエスケープ
- 原則:出力時に文脈エスケープ。PHPなら:
// 要素内容(テキストノード)
echo htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
- ポイント:
ENT_QUOTES
で"
と'
を両方エスケープ。第3引数でUTF-8を明示。
5.2 HTML属性値のエスケープ+ダブルクォートで囲む
// 属性値
printf('<img alt="%s">', htmlspecialchars($alt, ENT_QUOTES, 'UTF-8'));
- URL属性(
href
,src
,formaction
等)はスキーム許可リスト(https://
のみ等)で検証。javascript:
やdata:
を禁止。
5.3 HTTPレスポンスの文字エンコーディングを明示
Content-Type: text/html; charset=UTF-8
- ブラウザの自動判定を抑止。フルバイト攻撃や文字化け由来のバイパスを防ぐ。
6. 追加的な対策3つ(実務で必須に近い)
6.1 JavaScript内に値を埋め込む場合
- HTMLエスケープは効かない。JS文字列/JSONとして安全に埋め込む:
// 安全なJSONとして埋め込み(タグ/クォートをHEX化)
echo '<script>const DATA = ' .
json_encode($value, JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT) .
';</script>';
- DOM組み立ては
textContent
/createTextNode
/setAttribute
を基本に。innerHTML
は避ける。
6.2 HTTPOnly Cookie
- セッションIDはJSから読めないようHttpOnlyで配布:
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=...;
-
Secure
(HTTPS限定)・SameSite
(CSRF軽減)も併用。
6.3 CSP(Content Security Policy)
- スクリプトの読み込み元・実行方式を制御:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-<ランダム値>' https://trust.cdn.example;
object-src 'none'; base-uri 'self'; frame-ancestors 'self';
- インラインJSはnonce方式へ移行(
<script nonce="...">
)。unsafe-inline
は極力避ける。
7. 文脈別サニタイズ早見表(比較)
文脈 | 安全な出力方法/API | 追加検証 | NG例(避ける) |
---|---|---|---|
HTML本文(テキスト) |
htmlspecialchars(…, ENT_QUOTES, 'UTF-8') / テンプレートの自動エスケープ |
文字数・型 | 未エスケープ出力、innerHTML 直書き |
HTML属性値 | 同上+必ずクォートで囲む | URLはhttps:// 等のスキーム白リスト
|
非クォート属性、javascript: 許容 |
URL(href/src ) |
文字列エスケープ+allowlist | 相対/絶対の許可範囲 |
data: 、vbscript: 、javascript:
|
JS文字列/JSON |
json_encode(..., JSON_HEX_*) / エスケープ |
期待型(配列/数値) | 文字列連結での直埋め |
DOM操作 |
textContent /createTextNode /setAttribute
|
- |
innerHTML /outerHTML /insertAdjacentHTML
|
CSS | 可能なら埋め込まない | クラス/IDはサーバ側で割当 |
style 直書き、expression() 互換 |
8. 具体例で学ぶ「危険→安全」書き換え
危険(検索語の表示)
echo "検索: " . $_GET['q'];
安全
echo '検索: ' . htmlspecialchars($_GET['q'] ?? '', ENT_QUOTES, 'UTF-8');
危険(画像URLを属性に直埋め)
printf('<img src=%s>', $_GET['img']);
安全
$img = $_GET['img'] ?? '';
if (!preg_match('#^https://#', $img)) { $img = '/images/placeholder.png'; }
printf('<img src="%s" alt="">', htmlspecialchars($img, ENT_QUOTES, 'UTF-8'));
危険(JS内にユーザー名を連結)
<script>const name = '<?= $_GET['name'] ?>';</script>
安全
echo '<script>const name = ' .
json_encode($_GET['name'] ?? '', JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT) . ';</script>';
9. よくある落とし穴(レビュー時の着眼点)
-
自動エスケープの過信:テンプレートの例外(「生HTML」出力、
safe
/raw
/v-html
/dangerouslySetInnerHTML
)が紛れていないか。 -
文字コード未指定:
Content-Type
でcharset=UTF-8
を返しているか。HTML5の<meta charset>
は補助。 -
属性の非クォート:
<a href=${x}>
のような書き方。必ず"…"
で囲む。 -
URL検証欠如:
javascript:
やdata:
の混入。 -
DOM APIの誤用:便利だからと
innerHTML
。まずtextContent
を選ぶ。 - サニタイザ任せ:HTMLサニタイザ(例: コメント/タグ除去)は最後の手段。まず文脈エスケープを徹底。
10. ミニ演習:手元で赤信号を見つける
- テキストノード:
"><img src=x onerror=alert(1)>
をそのまま出すとどうなる? - 属性値:
" onerror="alert(1)
が混入したら? - URL:
javascript:alert(1)
を弾けている? - JS埋め込み:
'</script><script>alert(1)</script>
をjson_encode
で安全にできる?
これらに破綻しない実装になっていれば、XSS耐性は概ね合格ライン。
11. まとめ(チェックリスト)
-
要素内容は
htmlspecialchars(…, ENT_QUOTES, 'UTF-8')
を出力時に適用 - 属性値も同様にエスケープし、必ずクォートで囲む
-
Content-Type
でcharset=UTF-8
を明示 -
JS埋め込みは
json_encode(JSON_HEX_*)
等でJSコンテキスト用にエスケープ -
セッションCookieは
HttpOnly; Secure; SameSite
を付与 -
CSPで
script-src
を制御、インラインはnonce運用 -
危険なDOM API(
innerHTML
等)を避け、textContent
系を使う - URL属性は許可スキームのホワイトリストで検証
補足図:安全なレンダリングの考え方
[入力]
↓ (サーバで型/長さ/スキーム検証)
[出力時の文脈判定]
├─ HTML本文 → HTMLエスケープ
├─ HTML属性 → HTMLエスケープ+クォート
├─ URL属性 → エスケープ+スキーム検証
├─ JS埋め込み → JSON/JSエスケープ
└─ DOM操作 → textContent等の安全API
↓
[CSP/HttpOnly/SameSite で最終防御]
このノートをプロジェクトのレビュー基準や実装テンプレに落とし込めば、XSSは大きく減らせます。実務は「文脈エスケープ+安全API+ヘッダで締める」の三段構えでいきましょう。