XSSとは何か、なぜ今でも脅威なのか
「XSSなんて昔の話でしょ?」と思っているエンジニアは少なくありません。しかし、OWASP(Open Web Application Security Project)が毎年公表するトップ10脆弱性リストには、依然としてXSSが上位に顔を出しています。ブラウザのセキュリティ機能が進化し、フレームワークが普及した現代においても、XSSは現役の攻撃ベクターです。
XSS(Cross-Site Scripting)とは、悪意のあるスクリプトをWebページに埋め込み、閲覧したユーザーのブラウザ上で実行させる攻撃手法です。「クロスサイト」という名前の由来は、攻撃者が用意した悪意あるサイトを経由して、正規のサイトのコンテキストでスクリプトを動かすという初期の手口にあります。現在では単に「スクリプトをページに注入する攻撃全般」を指すようになっています。
XSSが怖いのは、被害の範囲が広い点です。Cookieの窃取によるセッションハイジャック、フィッシングフォームの表示、キーロガーの設置、さらにはブラウザを踏み台にした内部ネットワークへのアクセスまで、ひとつの脆弱性からさまざまな攻撃に発展する可能性があります。
XSSの3種類を整理する
XSSは発生の仕組みによって大きく3種類に分類されます。それぞれの特徴を理解することが、適切な対策を講じる第一歩です。
反射型XSS(Reflected XSS)
反射型XSSは、URLのクエリパラメータなどに含まれた悪意あるスクリプトが、サーバーを経由してそのままHTMLに埋め込まれて返ってくる攻撃です。たとえば、検索フォームの結果ページに「〇〇を検索しました」と表示する機能があるとします。URLが ?q=<script>alert(1)</script> だった場合、サニタイズせずにそのまま表示すると、スクリプトが実行されてしまいます。
攻撃者はこの悪意あるURLをメールやSNSで配布し、クリックしたユーザーのブラウザでスクリプトを実行させます。サーバーにはデータが保存されないため、ログに残りにくく検出が難しいという特徴があります。
蓄積型XSS(Stored XSS)
蓄積型XSS(永続型XSSとも呼ばれます)は、攻撃コードがデータベースなどに保存され、ページを閲覧したすべてのユーザーに影響が及ぶタイプです。SNSの投稿欄や掲示板のコメント欄など、ユーザー入力をそのまま表示する機能が標的になります。
一度悪意あるデータが登録されると、そのページを訪れるすべてのユーザーが被害を受けます。影響範囲が広く、特に管理者画面で実行されると権限昇格にもつながるため、3種類の中で最も危険度が高いとされています。
DOMベースXSS(DOM-based XSS)
DOMベースXSSは、サーバーを介さずクライアントサイドのJavaScriptが、URLのフラグメント(#以降)やlocation.searchなどからデータを読み取り、DOMに書き込む際に発生します。サーバーへのリクエストが発生しないため、サーバー側のログには記録されません。
// 脆弱なコード例
const hash = location.hash.slice(1);
document.getElementById("output").innerHTML = hash; // 危険!
SPAの普及に伴い、JavaScriptコードの複雑化とともにDOMベースXSSのリスクも高まっています。フレームワークを使っていても、innerHTML や document.write などの生のDOM操作を安易に使うと発生します。
3種類の比較
| 種類 | スクリプトの保存先 | 影響範囲 | 検出難易度 |
|---|---|---|---|
| 反射型 | 保存されない(URLのみ) | URLを踏んだユーザーのみ | 中程度 |
| 蓄積型 | データベース等に永続 | ページ閲覧者全員 | 低い(再現性あり) |
| DOMベース | 保存されない(クライアント処理) | URLを踏んだユーザーのみ | 高い(ログなし) |
具体的な対策を実装レベルで理解する
XSS対策は「入力のバリデーション」と「出力のエスケープ」が基本です。どちらか一方ではなく、両方を組み合わせることが重要です。
出力時のエスケープ
最も重要かつ基本的な対策は、HTMLとして解釈されうる文字を出力する際にエスケープすることです。特殊文字を対応するHTMLエンティティに変換します。
| 文字 | エスケープ後 |
|---|---|
< | < |
> | > |
& | & |
" | " |
' | ' |
現代のテンプレートエンジン(Jinja2、Blade、Thymeleafなど)はデフォルトで自動エスケープを行います。しかし、「生のHTMLとして出力する」機能(Jinja2の |safe フィルタ、BladeのユーザーデータへのHTMLを許可する箇所など)を使う際は特に注意が必要です。
Content Security Policy(CSP)の設定
CSPはHTTPレスポンスヘッダーとして設定し、ブラウザに「このページはどのリソースしか読み込まない」というポリシーを伝える仕組みです。XSSが発生してもインラインスクリプトの実行をブロックすることができます。
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'
ただし、CSPはあくまで多層防御の一環であり、エスケープを代替するものではありません。script-src 'unsafe-inline' を許可してしまうとXSSの防御効果が大幅に低下します。
フレームワークの機能を正しく使う
ReactやVue.jsなどの現代的フレームワークは、デフォルトでテキストをエスケープして出力します。しかし、以下のような「抜け穴」を使うと保護が無効になります。
- React:
dangerouslySetInnerHTMLの安易な使用 - Vue.js:
v-htmlディレクティブへのユーザー入力の直接埋め込み - Angular:
[innerHTML]バインディング
これらを使わざるをえない場合は、DOMPurifyなどのサニタイズライブラリでユーザー入力を浄化してから渡すのが定石です。
HttpOnly属性とSecure属性
XSSによるCookie窃取を防ぐため、セッションCookieには必ず HttpOnly 属性を付与しましょう。これによりJavaScriptからCookieへのアクセスがブロックされます。Secure 属性とあわせて設定することで、HTTPS通信時のみCookieが送信されるようになります。
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict
よくある誤解と落とし穴
「ReactやVueを使っているから安全」という思い込みは危険です。フレームワークが保護してくれるのはあくまでデフォルトの描画パスだけです。サードパーティライブラリや自前のDOM操作が混入すると、フレームワークの保護は無効化されます。
また、「入力時にタグを除去すればよい」という考え方にも落とし穴があります。エンコードやデコードのタイミングによっては、フィルタリングをバイパスされることがあります。たとえば <script> というHTMLエンティティが特定の処理を経てデコードされ、最終的に <script> として解釈されるケースがあります。フィルタリングより、出力時のエスケープを徹底する方が堅牢です。
セキュリティテストの実践
実装後はセキュリティテストも行いましょう。手軽にできるものとして、OWASP ZAPやBurp Suiteを使った自動スキャンが有効です。また、Chrome DevToolsを使って手動でペイロードを試すことも有効です。代表的なテストペイロードとして以下のようなものがあります。
<script>alert('XSS')</script>
"><script>alert('XSS')</script>
<img src=x onerror=alert('XSS')>
javascript:alert('XSS')
開発環境でこれらを入力フォームや URLパラメータに試し、アラートが表示されないことを確認しましょう。CI/CDパイプラインにSAST(静的解析)ツールを組み込んで、コードレビュー時に自動検出する体制も効果的です。
まとめ
XSSは「枯れた脆弱性」に見えて、依然として多くのWebアプリで発見されています。基本的な対策は決して難しくありません。出力時のエスケープを徹底し、フレームワークの安全な使い方を守り、CSPやCookieの属性で多層防御を実装する。この3点を意識するだけで、XSSのリスクは大幅に減らせます。
セキュリティは一度対策して終わりではありません。依存ライブラリの更新、コードレビューでのチェック、定期的なペネトレーションテストを組み合わせて、継続的に守り続ける姿勢が大切です。あなたが書くコードが、ユーザーを守る盾になります。