悪夢の「格納型XSS」:データベースを汚染する見えない凶器と、その防ぎ方
現場でインシデント対応をしていると、最も頭を抱えるのがこの「格納型(Stored)XSS」だ。反射型XSSが「通りすがりの暴漢」なら、格納型は「井戸に毒を撒くテロリスト」に等しい。
一度データベースに不正なスクリプトが混入すれば、そのページを開くすべてのユーザーが餌食になる。管理画面のダッシュボードに仕込まれた日には、管理者権限が奪取され、システム全体が乗っ取られることも珍しくない。今日は、教科書的な「サニタイズしましょう」という綺麗事ではなく、現場で通用する泥臭い防衛戦術について話そう。
—
なぜ「保存時サニタイズ」だけでは不十分なのか
多くのエンジニアが犯す最大の過ちは、「DBに入れる前に全部エスケープ(サニタイズ)すれば安全だ」と思い込むことだ。
しかし、考えてみてほしい。もし将来的にそのデータを「メール本文」や「PDF生成」、「外部APIへの連携」に使うことになったら? データベースに格納された時点でエスケープ済みだと、出力先によっては二重エスケープになったり、逆にエスケープが不十分で攻撃が成立したりする。
鉄則:保存時は「バリデーション(妥当性検証)」を行い、出力時に「エスケープ(文脈依存の無害化)」を行う。これが現代のセキュリティの黄金律だ。
—
実践:PHPにおける安全な実装パターン
バックエンドで最も重要なのは、入力値が期待した形式(型、長さ、文字種)に合致しているかを厳格にチェックすることだ。
500) {
throw new Exception(“不正な入力です。”);
}
// 2. DBへの保存はプリペアドステートメント一択
// ここでエスケープに頼ってはいけない。パラメータ化でSQLインジェクションを防ぐ
$stmt = $pdo->prepare(“UPDATE profiles SET bio = :bio WHERE user_id = :id”);
$stmt->execute([‘bio’ => $user_input, ‘id’ => $_SESSION[‘user_id’]]);
}
// 3. 出力時:テンプレートエンジン(Twig等)または htmlspecialchars を使用
// ※ここが防御の最前線
echo htmlspecialchars($user_bio, ENT_QUOTES, ‘UTF-8’);
?>
—
ブラウザを物理的に黙らせる「Content Security Policy (CSP)」
コードのバグは必ず発生する。その「万が一」のために、ブラウザ側でスクリプトの実行を制限するCSPをヘッダーに仕込んでおこう。これは、XSSが成功しても「実行させない」ための最終防衛ラインだ。
Nginxの設定例:
全てのリソースを自分自身のドメインからのみ許可する(インラインスクリプトを禁止)
add_header Content-Security-Policy “default-src ‘self’; script-src ‘self’; object-src ‘none’; frame-ancestors ‘none’;”;
これを設定しておけば、万が一脆弱性のあるコードが残っていても、攻撃者が仕込んだ はブラウザによって即座にブロックされる。
—
後輩エンジニアへ:意識すべき「盲点」
格納型XSSのPoC(概念実証)を行う際、攻撃者は必ずしも タグを使わない。「隠れ蓑」を多用するんだ。
- 属性への注入:
- JavaScriptプロトコル:
プロフィール
これらを見逃さないためには、「HTMLのどの文脈(タグの中、属性の中、JSの中)にデータが埋め込まれるか」を常に意識する必要がある。特に、最近のモダンフロントエンド(React/Vue)を使っていれば安心だと思っているなら大間違いだ。dangerouslySetInnerHTML や v-html を不用意に使っていないか、今すぐリポジトリをgrepして確認してくれ。
まとめ:セキュリティは「多層防御」
1. 入力時はバリデーション: 型、長さ、文字種で弾く。
2. 保存時はプリペアドステートメント: SQLiを防ぐ。
3. 出力時はコンテキストに応じたエスケープ: これが最大の防御。
4. CSPでブラウザを縛る: 攻撃を物理的に無効化する。
セキュリティに「これで完璧」はない。あるのは「攻撃のコストを極限まで引き上げるための積み重ね」だけだ。今日から自分の書くコードが「攻撃者にとってどれだけ面倒なものか」を意識してみてほしい。それが、一流のエンジニアへの第一歩だ。
コメント