未検証のサーバー側URLリダイレクト

未検証のリダイレクトと転送(オープンリダイレクト)

説明

オープンリダイレクトは、サーバーがユーザー入力を十分に検証せずにリダイレクト先として使うことで発生します。攻撃者は正規のドメインに見えるリンクをクリックさせ、パラメーターで外部の悪意あるサイトへ誘導できます。認証情報のフィッシングや、脆弱なOAuth/SSO処理での認可コード・トークンの横取りにつながるおそれがあります。

想定される影響

  • 信頼できるように見えるリンクを使った認証情報のフィッシング
  • 認証処理が攻撃者の指定する戻り先を受け入れる場合の、コードやトークンの横取り
  • 悪意あるダウンロードや攻撃用ページへの誘導
  • 最初のURLだけに依存するドメイン許可リストやメッセージフィルターの回避

対処方法

  • 内部への移動は事前に決めたローカルパスに限定してください。必要な外部URLは固定の対応表から選んでください。
  • 内部パスは許可リスト、外部URLは事前定義したキーを使って選択してください。
  • 検証に失敗した場合は / などの安全な既定の場所へ移動させてください。
  • 正規化した値を許可リストと厳密に比較してください。疑わしい部分文字列を拒否するだけの方法に依存しないでください。
  • Next.jsの NextResponse.redirect(new URL(..., request.url)) やApp Routerの redirect(...) に渡す値も、同じ基準で検証してください。

例

変更前

javascript
const express = require("express");
const app = express();

// 変更前: ユーザー入力にそのままリダイレクトします。
app.get("/go", (req, res) => {
  const target = req.query.u; // ユーザーが指定する入力です。
  return res.redirect(target); // 検証がありません(オープンリダイレクト)。
});

app.listen(3000);

変更後

javascript
const express = require("express");
const app = express();

// 変更後: 内部パスの許可リストと外部URLのキー対応表を使います。
const allowedPaths = new Set(["/home", "/dashboard", "/help"]);
const externalMap = new Map([
  ["docs", "https://docs.example.com"],
  ["status", "https://status.example.com"],
]);

app.get("/go", (req, res) => {
  const next = typeof req.query.next === "string" ? req.query.next.trim() : "";
  const siteKey = typeof req.query.site === "string" ? req.query.site.trim() : "";

  // 1) 事前定義した内部パスだけを許可します。
  if (allowedPaths.has(next)) {
    return res.redirect(next);
  }

  // 2) 外部の移動先はキーとURLの対応表からだけ選びます。
  const extUrl = externalMap.get(siteKey);
  if (extUrl) {
    return res.redirect(extUrl);
  }

  // 3) 検証に失敗したら安全な既定のパスへ移動します。
  return res.redirect("/");
});

app.listen(3000);

説明:

  • 変更前: 未検証の入力 u がLocationヘッダーに入り、任意のドメインへ誘導できるため、フィッシングに悪用されるおそれがあります。
  • 変更後: 内部の移動先は allowedPaths に含まれるパスだけです。クエリ値は文字列に限定し、外部の移動先は Map に登録した site キーから選びます。配列や継承プロパティは移動先になりません。不正な値は / へ誘導します。

参考資料