クライアント側のリクエスト偽造

検証しないリクエスト URL によるクライアント側のリクエスト偽造

説明

ブラウザー内でユーザー入力をそのまま使い、fetch、XHR、axios などの URL を組み立てると、クライアント側のリクエスト偽造につながる場合があります。攻撃者はクエリ文字列、ハッシュ、フォーム入力を操作し、../ などによる意図しないパスや、許可していないホストにリクエストを送信させる可能性があります。対象 API が状態を変更するリクエストを受け入れれば、応答を読めなくても影響が生じます。応答を innerHTML で描画すると XSS につながるおそれもあります。実際に送信できるか、どのような影響があるかは、ブラウザーのセキュリティポリシー、認証情報のオプション、対象 API の認証・認可によって異なります。

想定される影響

  • パスの操作や任意のホスト指定によって、内部 API や管理用エンドポイントへのリクエストを誘導されるおそれがあります。
  • 認証済みユーザーのブラウザーを使い、意図しない API 呼び出しでデータを取得・変更される可能性があります。
  • 信頼できない応答を innerHTML で描画すると、スクリプト実行やコンテンツの改ざんにつながるおそれがあります。
  • 127.0.0.1 や社内ホストなど、ユーザーのローカル・社内ネットワークに不要なリクエストを送信するために悪用される場合があります。

対処方法

  • URL のホストを固定するか、事前に定義した許可リストからだけ選択してください。
  • 入力全体の形式と許可範囲を検証してください。Number や parseInt で変換するだけでは検証は完了しません。../、//、%2e%2e などによるパス操作を防いでください。
  • 基準 URL を固定し、検証したパス要素だけを組み立ててください。URL や encodeURIComponent だけでは許可したパスになるとは限らないため、最終的な出所とパスも確認してください。
  • 応答の表示には innerHTML ではなく textContent を使用してください。HTML が必要な場合は、信頼できるサニタイザーを使ってください。
  • fetch の credentials、method などのオプションを必要な範囲に限定し、サーバー側の認証・認可と CSRF 対策を適用してください。CORS だけでは、リクエスト送信や状態変更を防げません。

例

変更前

javascript
async function showProfile() {
  const qs = new URLSearchParams(location.search);
  const next = qs.get("next"); // 例: https://evil.example/steal
  const userId = qs.get("id"); // 例: ../../admin/metrics

  // BAD 1: 信頼できない URL 全体を使用
  const r1 = await fetch(next);

  // BAD 2: 入力をパスに直接連結(パストラバーサルの可能性)
  const r2 = await fetch("/api/user/" + userId);
  const data = await r2.json();

  // BAD 3: 検証しない応答を innerHTML に挿入し、XSS の危険を生む
  document.getElementById("content").innerHTML = data.html;
}

変更後

javascript
async function showProfileSafe() {
  // 1) 許可したホストだけを使用
  const BASE = "https://api.example.com";

  // 2) パスパラメーターのスキーマ(数値)を強制、または厳密に検証
  const qs = new URLSearchParams(location.search);
  const rawId = qs.get("id");
  const id = Number(rawId);
  if (!Number.isInteger(id) || id < 0) {
    throw new Error("invalid id");
  }

  // 3) base を固定し、検証したパス要素だけを組み立てる
  const url = new URL(`/v1/user/${id}`, BASE);

  const res = await fetch(url.toString(), {
    method: "GET",
    credentials: "same-origin",
  });
  const data = await res.json();

  // 4) innerHTML ではなく textContent で内容を表示
  const box = document.getElementById("content");
  box.textContent = data.name; // HTML が必要なら信頼できるサニタイザーを使用
}

説明:

  • 変更前:
    • 入力(next、id)をそのまま fetch の URL に使うため、任意のホストへの送信やパストラバーサルが可能になります。
    • 応答を innerHTML に直接挿入すると、XSS につながるおそれがあります。
  • 変更後:
    • ホストを固定し、パスに 0 以上の整数だけを入れることで、../ などの操作を防ぎます。許可する ID の範囲とアクセス権は、サーバーでも検証する必要があります。
    • URL(base) で組み立て、ユーザー入力で URL 全体を置き換えられないようにします。
    • textContent により、HTML として実行せずテキストを表示します。

参考資料