# Next.jsのオープンリダイレクト対策 — 認証 callbackUrl / redirect() を検証する

> redirect() や callbackUrl にユーザー入力をそのまま渡すと、信頼できる自社ドメインを起点に攻撃者サイトへ誘導され、フィッシングや認証直後のトークン窃取に繋がります。相対パス強制・ホストallowlist・new URL での検証で安全側に倒す方法を、Next.jsの認証フローの脆弱→修正コードと、//evilやバックスラッシュ等のバイパス対策まで解説します。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: Next.js, セキュリティ, TypeScript
- URL: https://tomodahinata.com/blog/nextjs-open-redirect-callback-url-prevention-guide
- カテゴリ: アプリ層セキュリティ
- 総合ガイド: https://tomodahinata.com/blog/nextjs-supabase-application-security-guide

## 要点

- オープンリダイレクトは、信頼できる自社ドメインのURLを踏ませて攻撃者ドメインへ転送させる欠陥。リンクの見た目が自社ドメインのため、フィッシングや認証直後のトークン窃取の『信頼の踏み台』になる
- Next.jsでは `redirect(searchParams.next)`、ログイン後の `callbackUrl`、OAuthコールバックの戻り先、middlewareの `NextResponse.redirect` が典型的な発生箇所。`redirect()` は絶対URLを渡せば外部にも飛ぶ
- 単純な文字列チェックは破られる——`//evil.example`（プロトコル相対）、`/\evil`（バックスラッシュ）、`yourapp.com@evil.example`（userinfo）。防御は『相対パスのみ許可』を既定にし、`new URL` で解決してオリジンを照合する
- どうしても外部に飛ばすなら、hostを完全一致で照合する明示的なallowlistとスキーム固定（httpsのみ）にする。返す値は可能な限りパス部分だけに削る
- 正直に言うと——汚染入力→redirectシンクのデータフローはtaint解析で機械的に検出できるが、『どこへの遷移を許すか』という許可先の設計は、業務を知る人間にしか決められない

---

最初に結論を述べます。**オープンリダイレクトは、`redirect()` や `callbackUrl` にユーザー入力をそのまま渡したときに空く穴で、対策の本質は「遷移先を文字列として信じない」ことに尽きます。** 安全側の既定は「自オリジンの相対パスだけを許可する」。外部に飛ばす正当な理由がある経路だけ、ホストの完全一致allowlistとスキーム固定で例外的に許す——この2段構えで、機械的に潰せます。

これは難しい攻撃ではありません。URLのクエリに `?next=https://evil.example` と書くだけ。にもかかわらず本番に残りやすいのは、**自分のアカウントで触っている限り絶対に顕在化しない**からです。開発者もAIも「ログイン後に元のページへ戻る」というハッピーパスは作りますが、「戻り先に攻撃者URLを差し込まれたら」はデモでは誰も試しません。本記事は、その見落としがどこに空き、なぜ素朴なチェックでは塞げず、どう検証すれば安全側に倒せるのかを、Next.jsの認証フローの実コード（脆弱→修正）で解説します。OWASPはこれを古くから「Unvalidated Redirects and Forwards」として整理しており、現在も [OWASP Web Security Testing Guide](https://owasp.org/www-project-web-security-testing-guide/) の検査項目、[OWASP ASVS](https://owasp.org/www-project-application-security-verification-standard/) の検証要件、そして [OWASP Top 10](https://owasp.org/www-project-top-ten/) が説く入力検証の規律の中に位置づけられています。

---

## 1. オープンリダイレクトとは——信頼ドメインを「踏み台」にする転送

オープンリダイレクトは、**アプリが利用者の操作できる値を遷移先として受け取り、検証せずにそこへ転送してしまう**欠陥です。攻撃の起点になるリンクは、こういう形をしています。

```text
https://yourapp.com/login?next=https://evil.example/login
        ^^^^^^^^^^^^                ^^^^^^^^^^^^^^^^^^^^^^^^
        信頼される自社ドメイン        実際の着地先（攻撃者）
```

ユーザーがメールやSNSで見るのは `yourapp.com` という**正規のドメイン**です。ホバーしてもドメインは自社。メールのスパムフィルタも、URL検査も、自社ドメイン宛なので通します。ところがアクセスすると、アプリは `next` パラメータを信じて `https://evil.example/login` へ転送する。**ドメインの信頼が、丸ごと攻撃者に貸し出される**——これがオープンリダイレクトの本質です。

重要なのは、このリクエストが**HTTPとして完全に正常**だという点です。不正な文字列もインジェクションも含まない。`next` の値が「外部ホストである」という事実だけが問題で、それが攻撃かどうかは「このアプリがどの遷移先を許すか」というアプリ固有のルールでしか決まりません。だからWAFやセキュリティヘッダーでは構造的に止められず、コード側の検証が要ります。この「汚染入力→危険シンク」という構造は注入クラス全体に共通で、全体像は[Next.js × Supabase アプリケーションセキュリティ完全ガイド](/blog/nextjs-supabase-application-security-guide)に地図を描いています。

---

## 2. なぜ危険か——フィッシングと、認証直後のトークン窃取

オープンリダイレクト単体で「サーバーが乗っ取られる」ことはありません。怖いのは、これが**他の攻撃を成立させる『信頼の踏み台』**になることです。被害は大きく2系統あります。

### 2-1. フィッシング——ドメインの信頼を貸し出す

攻撃者は `https://yourapp.com/login?next=https://evil.example/login` を配布します。ユーザーは自社ドメインを信じてクリックし、一瞬で見分けのつかない偽ログイン画面（`evil.example`）に着地します。そこで入力した認証情報やワンタイムコードがそのまま盗まれる。**最初の一歩が正規ドメインである**ことが、フィルタもユーザーの警戒も突破します。

### 2-2. 認証直後のトークン窃取——OAuth/コールバックの戻り先

より深刻なのは、認証フローに絡む場合です。OAuth/OIDCでは、認可サーバーが発行する**認可コードやトークンは、登録済みの戻り先（redirect_uri）に届けられます**。通常はredirect_uriの厳密登録で任意ホストへの送信を防ぎますが、**許可リストに載った自社ホスト上にオープンリダイレクトがあると連鎖されます**。

```text
認可サーバー → https://yourapp.com/callback?next=https://evil.example
            （登録済みの自社ホストなので通る）
yourapp.com/callback → next を信じて evil.example へ転送
            → 認可コード／トークンがクエリ・フラグメント・Referer 経由で流出
```

アプリ独自の `callbackUrl`（ログイン後にユーザーを元の画面へ戻すための値）でも事情は同じです。ユーザーが**最も警戒を解く「認証に成功した直後」**に攻撃者サイトへ送られるため、二段階の偽装やセッションの引き継ぎに悪用されます。[OWASP ASVS](https://owasp.org/www-project-application-security-verification-standard/) が「許可されたURLにのみリダイレクトする（allowlist）か、信頼できない遷移先では警告を出す」ことを検証要件として求めているのは、まさにこの連鎖を断つためです。

---

## 3. Next.jsでオープンリダイレクトが空く4つの場所

Next.jsでは、遷移先を扱うAPIがいくつもあります。`next/navigation` の `redirect()`、Route Handlerの `NextResponse.redirect()`、middlewareの `NextResponse.redirect()`、そしてクライアントの `router.push()` / `window.location`。**そのどれもが、利用者の入力を素通しにすると穴になります。** 典型を脆弱コードで4つ挙げます。

### 3-1. Server Componentでの `redirect(searchParams.next)`

`next/navigation` の `redirect()` は、**絶対URLを渡せば外部にも飛びます**。Next.jsはこれを制限しません。安全性の責任は完全に呼び出し側にあります。

```tsx
// app/login/page.tsx — 脆弱：next をそのまま redirect に渡す
import { redirect } from "next/navigation";

export default async function LoginPage({
  searchParams,
}: {
  searchParams: Promise<{ next?: string }>;
}) {
  const { next } = await searchParams; // ← クライアントが自由に変えられる汚染入力
  const session = await getSession();
  if (session) {
    redirect(next ?? "/dashboard"); // next="https://evil.example" でも素直に飛ぶ
  }
  // …ログインフォームを表示
}
```

### 3-2. ログインServer Actionの `callbackUrl`

`"use server"` のアクションは実質POSTエンドポイントで、`formData` の値は等しくクライアント操作可能です。「画面に出していない隠しフィールドだから安全」は成立しません。

```ts
// app/login/actions.ts — 脆弱：callbackUrl を検証せず認証直後に飛ばす
"use server";
import { redirect } from "next/navigation";

export async function login(formData: FormData) {
  const callbackUrl = String(formData.get("callbackUrl") ?? "/dashboard");
  await signIn(formData); // 認証成功
  redirect(callbackUrl); // ← 最も信頼された瞬間に攻撃者サイトへ送られうる
}
```

### 3-3. OAuth/認証コールバックのRoute Handler

SupabaseやAuth.jsの戻りを受ける `app/auth/callback/route.ts` は、`next` を受け取ってセッション確立後に転送する定番です。ここが一番踏まれます。

```ts
// app/auth/callback/route.ts — 脆弱：戻り先を new URL に素通し
import { NextResponse, type NextRequest } from "next/server";

export async function GET(req: NextRequest) {
  const next = req.nextUrl.searchParams.get("next") ?? "/";
  await exchangeCodeForSession(req); // セッション確立
  return NextResponse.redirect(new URL(next, req.nextUrl.origin));
  // next="//evil.example" → new URL は https://evil.example に解決される
}
```

`NextResponse.redirect()` は絶対URLを要求するため `new URL(next, origin)` で組み立てがちですが、**`new URL` は第1引数が絶対URLやプロトコル相対だと第2引数の基準を上書きします**。これが次節のバイパスの温床です。

### 3-4. middlewareの `NextResponse.redirect()`

未認証を弾いて「戻り先付き」でログインへ送る、その戻り先 (`from`) を再利用するときに同じ穴が空きます。

```ts
// middleware.ts — 脆弱：戻り先 from を検証せず redirect
import { NextResponse, type NextRequest } from "next/server";

export function middleware(req: NextRequest) {
  const from = req.nextUrl.searchParams.get("from");
  if (from) return NextResponse.redirect(new URL(from, req.url)); // 検証なし
  // …
}
```

なお、middlewareの `NextResponse.rewrite()` に利用者制御のURLを渡すのは**オープンリダイレクトではなくSSRF**（自社エッジが内部資源へ到達する）に化けます。こちらは別クラスの問題で、[Next.jsのSSRF対策](/blog/nextjs-ssrf-prevention-server-actions-route-handlers-guide)で扱っています。

---

## 4. なぜ単純なチェックは破られるのか——バイパスの体系

「`http` で始まっていなければ相対パスだろう」「自社ドメインを含んでいればOK」——こうした**文字列ベースの素朴なチェックは、ことごとく回避されます**。代表的なバイパスを整理します（攻撃者ドメインは `evil.example`、自社は `yourapp.com` とします）。

| 入力例 | 素朴なチェックの判定 | ブラウザ/URLパーサの実際の解釈 |
|---|---|---|
| `/dashboard` | 通過（安全） | 自オリジンのパス（正規） |
| `//evil.example` | `startsWith("/")` を通過 | プロトコル相対 → `https://evil.example` |
| `/\evil.example`（円記号） | `startsWith("//")` を回避 | http(s)系では `\` が `/` 扱い → `//evil.example` |
| `https://yourapp.com@evil.example` | 文字列に自社名を含む | `@` の前はuserinfo、hostは `evil.example` |
| `https://yourapp.com.evil.example` | `startsWith("https://yourapp.com")` を通過 | hostは `yourapp.com.evil.example` |
| `https://evil.example/yourapp.com` | `includes("yourapp.com")` を通過 | hostは `evil.example` |
| `javascript:alert(document.cookie)` | スキーム検証が無いと通過 | クライアント遷移なら実行されうる |

罠の核心は2つです。

ひとつは **`//` と `\`**。`//evil.example` はスキームを省いた「プロトコル相対URL」で、ブラウザは現在のスキームを補って `https://evil.example` として扱います。`startsWith("/")` のチェックは通過してしまう。さらに厄介なのが**円記号 (`\`)** です。WHATWGのURL標準では、http/httpsのような特別なスキームにおいて `\` は `/` と同じに正規化されるため、`/\evil.example` は `//evil.example` と等価になり、`startsWith("//")` という対策をすり抜けます。

もうひとつは **`@`（userinfo）とサブドメイン詐称**。`https://yourapp.com@evil.example` は、`@` の前がユーザー情報、ホストは `evil.example` です。文字列に自社ドメインが「含まれているか」を見るチェックは全滅します。host を**完全一致**で見ない限り、`yourapp.com.evil.example` や `evil.example/yourapp.com` も通ってしまいます。

結論はこうです。**遷移先は「文字列」ではなく「構造（origin/host/scheme）」で判断しなければならない。** そしてその構造を、ブラウザと同じルールで解釈できる道具が `new URL` です。

---

## 5. 正しい対策——相対パス強制・`new URL`検証・host allowlist

### 5-1. 既定は「自オリジンの相対パスだけ」を許可する

ほとんどのログイン後遷移は、**自分のアプリ内のパスに戻るだけ**です。ならば「自オリジンの相対パス以外は全部捨てる」のが最も堅く、最も単純です。鍵は、**ブラウザと同じパーサ（`new URL`）で解決し、オリジンが一致するかだけを見る**こと。文字列の前方一致は一切使いません。

```ts
// lib/safe-redirect.ts — 自オリジン上の相対パスだけを許可する（既定の防御）
export function toSafeRelativePath(
  input: string | null | undefined,
  fallback = "/dashboard",
): string {
  if (!input) return fallback;

  // 実在しない予約TLDを基準オリジンにする。許可ホストと偶然一致しないため安全。
  const base = "https://placeholder.invalid";
  let url: URL;
  try {
    url = new URL(input, base); // ブラウザと同じ規則で解決（\→/ 正規化等もここで吸収）
  } catch {
    return fallback; // パース不能な値は捨てる
  }

  // 別ホストに化けたら拒否。これ1つで //evil・/\evil・@evil・絶対URL を全部弾く。
  if (url.origin !== base) return fallback;

  // 万一に備え、絶対URLは絶対に返さない。パス成分だけに削る。
  return url.pathname + url.search + url.hash;
}
```

なぜこれで前節のバイパスが全滅するのかを確かめます。

- `//evil.example` → 解決すると origin が `https://evil.example` になり、`base` と不一致 → 拒否。
- `/\evil.example` → `\` が `/` に正規化され `//evil.example` と等価 → origin 不一致 → 拒否。
- `https://yourapp.com@evil.example` → host が `evil.example` → origin 不一致 → 拒否。
- `/dashboard?tab=1` → origin が `base` と一致 → `"/dashboard?tab=1"` を返す（正規）。

**「許可ホストの判定」を自前の文字列処理で書かず、URLパーサに委ねる**のがこの設計の肝です。さらに最後に `pathname + search + hash` だけを返すことで、たとえ将来オリジン判定をすり抜ける入力が現れても、**絶対URLが出力に混じる経路自体を断って**います（多層防御）。

### 5-2. 外部に飛ばす正当な理由があるなら、host完全一致のallowlist

決済プロバイダや系列ドメインへ戻すなど、**本当に外部へリダイレクトする要件**もあります。その場合だけ、明示的なallowlistで例外を許します。ここでも判定は構造に対して行い、host は**完全一致**、スキームは **https に固定**します。

```ts
// lib/safe-redirect.ts — 外部遷移は明示的 allowlist でのみ許可する
const ALLOWED_HOSTS = new Set([
  "app.example-corp.com",
  "docs.example-corp.com",
]);

export function toAllowlistedUrl(
  input: string | null | undefined,
  fallback = "/",
): string {
  if (!input) return fallback;

  let url: URL;
  try {
    url = new URL(input); // 絶対URL必須。相対は throw → fallback
  } catch {
    return fallback;
  }

  if (url.protocol !== "https:") return fallback; // スキームを固定（javascript: 等を排除）
  if (!ALLOWED_HOSTS.has(url.hostname.toLowerCase())) return fallback; // host を完全一致で照合
  return url.toString();
}
```

`url.hostname` を**小文字化して `Set` と完全一致**させるのが要点です。`endsWith(".example-corp.com")` のような末尾一致は `evilexample-corp.com` を通すため使いません。サブドメインまで許すなら、`hostname === "example-corp.com" || hostname.endsWith(".example-corp.com")` のように**先頭のドットでアンカー**します。

### 5-3. Next.jsの各経路に適用する

検証関数を用意したら、3節の脆弱コードは「**検証してから飛ばす**」だけで塞がります。

```ts
// app/auth/callback/route.ts — 修正：検証してから redirect
import { NextResponse, type NextRequest } from "next/server";
import { toSafeRelativePath } from "@/lib/safe-redirect";

export async function GET(req: NextRequest) {
  const next = toSafeRelativePath(req.nextUrl.searchParams.get("next"));
  await exchangeCodeForSession(req);
  // next は同一オリジンの相対パスであることが保証されているので new URL も安全
  return NextResponse.redirect(new URL(next, req.nextUrl.origin));
}
```

```ts
// app/login/actions.ts — 修正：callbackUrl を相対パスに正規化してから redirect
"use server";
import { redirect } from "next/navigation";
import { toSafeRelativePath } from "@/lib/safe-redirect";

export async function login(formData: FormData) {
  const callbackUrl = toSafeRelativePath(formData.get("callbackUrl")?.toString());
  await signIn(formData);
  redirect(callbackUrl); // 自オリジンのパスにしか飛ばない
}
```

Server Component や middleware も同様に、`searchParams` から取り出した直後に `toSafeRelativePath()` を通すだけです。**検証は「入力を受け取った場所」で1回、遷移の直前に行う**——これが規律です。

> **エンコードの扱いに注意。** `searchParams.get()` や `formData.get()` は**すでにパーセントデコード済みの値**を返します。だから `%2f%2fevil`（=`//evil`）はこの時点で復号されており、上の検証で正しく弾けます。逆に、デコード前の生文字列を別の場所で再利用すると二重デコードでズレるため、**「パース済みのAPIから取り出した値」を検証し、それをそのまま使う**のが安全です。

### 5-4. バイパス入力を回帰テストに固定する

検証関数は「一度書いて終わり」にしてはいけません。**前節のバイパス入力をそのままテストに固定**しておけば、誰か（人間でもAIでも）が「相対パスだけだから」と検証を緩めた瞬間に、CIが赤くなって退行を止めます。`new URL` ベースの検証は副作用のない純粋関数なので、vitestで安く・速くテストできます。

```ts
// lib/safe-redirect.test.ts — バイパス入力が確実に弾かれることを固定する
import { describe, expect, it } from "vitest";
import { toSafeRelativePath } from "./safe-redirect";

describe("toSafeRelativePath", () => {
  it("自オリジンの相対パスは通す", () => {
    expect(toSafeRelativePath("/dashboard?tab=1")).toBe("/dashboard?tab=1");
  });

  // 第4節のバイパス表を、そのまま実行可能な仕様として固定する
  it.each([
    "//evil.example", // プロトコル相対
    "/\\evil.example", // バックスラッシュ（\→/ 正規化で //evil 相当）
    "https://yourapp.com@evil.example", // userinfo 詐称
    "https://yourapp.com.evil.example", // サブドメイン詐称
    "javascript:alert(document.cookie)", // 危険スキーム
  ])("バイパス入力 %s は fallback に倒す", (input) => {
    expect(toSafeRelativePath(input)).toBe("/dashboard");
  });

  it("空・null・undefined は fallback に倒す", () => {
    expect(toSafeRelativePath(null)).toBe("/dashboard");
    expect(toSafeRelativePath("")).toBe("/dashboard");
    expect(toSafeRelativePath(undefined)).toBe("/dashboard");
  });
});
```

テストが緑なら「**よくあるバイパスは弾けている**」ことの機械的な証拠になります。ただし——これは「許可先の設計が正しい」ことの証明ではありません（第7節）。テストが守るのは「壊れていないこと」であって、「仕様が正しいこと」ではない。この区別は最後まで意識してください。

---

## 6. クライアント遷移の追加リスク——`javascript:` と `location`

サーバーのHTTPリダイレクト（`Location` ヘッダ）では `javascript:` スキームは実行されません。しかし**クライアント側の遷移**——`window.location.href = next` や `router.push(next)`、`<a href={next}>`——では話が変わります。

```tsx
"use client";
// 脆弱：javascript: / data: スキームがそのまま実行されうる（DOM XSS）
function goBack(next: string) {
  window.location.href = next; // next="javascript:alert(document.cookie)" で発火
}
```

つまりクライアント遷移は、**オープンリダイレクト（外部ホストへの転送）に加えて、スキーム経由のスクリプト実行（DOM XSS）**という二重のリスクを負います。対策は同じく `toSafeRelativePath()` で相対パスに正規化してから渡すこと。外部URLを許す場合も、`https:` 以外のスキームを `toAllowlistedUrl()` で確実に排除します。この `javascript:` / `data:` スキームの実行は本質的にDOM XSSの一種であり、その仕組みと対策は[DOM XSSとdangerouslySetInnerHTMLの対策](/blog/nextjs-xss-dom-xss-dangerouslysetinnerhtml-prevention-guide)で詳しく掘り下げています。

---

## 7. taintで検出する——汚染入力→redirectシンク

オープンリダイレクトは、**「クライアントが操作できる入力（source）が、検証されないまま遷移シンク（sink）に到達する」**という共通構造を持ちます。だから正規表現ではなく、関数内のデータフロー（taint解析）で機械的に追えます。

| 汚染入力（source） | 危険シンク（sink） |
|---|---|
| `searchParams.get("next")` | `redirect(next)` |
| `params` / `formData.get("callbackUrl")` | `NextResponse.redirect(new URL(next, …))` |
| `req.nextUrl.searchParams.get("from")` | `router.push(next)` / `window.location = next` |
| `cookies().get("returnTo")` | `<a href={next}>` |

私が公開しているOSS Aegisは、この「source→（検証を経ずに）sink」のパスを検出します。インストール不要・設定不要で走ります。

```bash
# 汚染入力→redirectシンクのデータフローを可視化する
npx @aegiskit/cli scan
```

検出ロジックは、`searchParams.get` 等の汚染源から `redirect` 系シンクまでの経路に、**`new URL` での解決＋オリジン照合や allowlist 照合のような『検証ノード』が挟まっているか**を見ます。挟まっていなければ「未検証のリダイレクト」として指摘します。

> **正直なスコープ。** taint解析が機械的に見つけられるのは「**検証なしで入力が遷移シンクに届いている**」という事実までです。「**どの遷移先を許すべきか**」——allowlistに載せるホスト、ログイン後にどこへ戻すかという業務上の正解——は、ツールには決められません。それはあなたのアプリの仕様であり、コードの外形からは導けない設計判断だからです。データフロー解析は関数内（intraprocedural）が基本で、モジュールやフレームワークを跨ぐ流れは見逃します。**クリーンな結果は「よくある罠は踏んでいない」であって「安全」ではありません。** この検出は、人間のレビューと脅威モデリングを置き換えるものではなく、補完するものです。

---

## 8. 本番前チェックリスト

外注でもAI製でも、本番投入の前に最低限これだけは確認してください。

- [ ] `redirect()` / `NextResponse.redirect()` / `router.push()` に、**`searchParams`・`formData`・`cookies` 由来の値を素通しで渡していない**
- [ ] 既定は **`new URL` で解決しオリジン照合した相対パスだけ**を許可している（前方一致チェックではない）
- [ ] 外部遷移は **host完全一致のallowlist＋スキーム固定（https）** でのみ許可している
- [ ] `//evil`・`/\evil`・`yourapp.com@evil`・`yourapp.com.evil` の**バイパス入力を実際に試した**（200で自社に留まるか）
- [ ] **認証コールバック（`/auth/callback` 等）の `next`** を検証している（最も漏れやすい経路）
- [ ] **ログイン後の `callbackUrl`** を検証している（認証直後の最も危険な遷移）
- [ ] クライアント遷移で **`javascript:` / `data:` スキームを排除**している（DOM XSS対策）
- [ ] middlewareの `redirect` の戻り先 (`from`) を検証している。`rewrite` に利用者URLを渡していない
- [ ] **taintスキャン（`npx @aegiskit/cli scan`）をCIに常設**し、未検証リダイレクトの再混入を止めている

発注者の視点で最も効くのは、**「`?next=//evil.example` を付けたらどこに飛びますか？」「ログイン後の戻り先はどうやって検証していますか？」**の2問です。良い開発者は即答できます。

---

## 9. どこまで自分で、どこから監査か

正直に線を引きます。

**「未検証のリダイレクトを見つける」ところまでは、自動化で機械的に潰せます。** 汚染入力から遷移シンクへの経路を追うtaint解析をCIに入れ、`toSafeRelativePath()` / `toAllowlistedUrl()` のような検証関数を1か所に集約すれば、人間が毎回考える必要はありません。まずは [Aegis](/aegis)（無料OSS、`npx @aegiskit/cli scan`）で現状を可視化するのが、最もコスパの良い第一歩です。

一方、**「どこへの遷移を許すか」という許可先の設計は、人間の領域です。** allowlistに載せるべきホスト、認証後の正しい戻り先、外部決済からの復帰フロー——これらはあなたの業務とデータモデルを理解した人間にしか決められません。ここで「導入すれば安全」と言い切るツールは、むしろ危険です。**Aegisは未検証リダイレクトを検出・警告しますが、許可先の設計が正しいことは証明しません。完全に安全にする魔法はありません。**

だからこそ線引きが要ります。どこまで自分で固め、どこから専門家のレビューが要るか——既存のNext.jsアプリで認証フローやリダイレクトの設計レビューが必要なら、[セキュリティ監査](/aegis/audit)で承ります。私自身、[木材流通業界のDX案件](/case-studies/lumber-industry-dx)で、認証・テナント分離・遷移制御を含むアプリ層の認可を実運用で設計・検証してきました。

---

## よくある質問（FAQ）

**Q. `redirect()` に相対パスしか渡さなければ、検証は不要ですか？**
A. その値が**定数なら**不要です。問題になるのは `searchParams` や `formData` 由来の**動的な値**を渡すときだけ。動的な値は短い相対パスに見えても `//evil` や `/\evil` に化けるため、必ず `toSafeRelativePath()` を通してください。

**Q. allowlistは正規表現で書いてはいけませんか？**
A. 避けるべきです。`/yourapp\.com/` のような正規表現は `yourapp.com.evil.example` にもマッチします。`new URL` で `hostname` を取り出し、`Set` で**完全一致**照合するのが安全です。サブドメインを許すなら先頭のドットでアンカーします。

**Q. プロトコル相対 (`//`) さえ弾けば十分ですか？**
A. 不十分です。円記号 (`/\evil`) は `\`→`/` の正規化で `//evil` と等価になり、`startsWith("//")` をすり抜けます。さらに `@`（userinfo）やサブドメイン詐称もあります。個別パターンを潰すより、**`new URL` で解決してオリジンを照合する**1つの判定に寄せるのが堅実です。

**Q. Auth.js（NextAuth）やSupabaseを使っていれば自動で安全ですか？**
A. フレームワークの既定の `redirect` コールバックは多くの場合、同一オリジン/相対のみ許可します。しかし**カスタムの `redirect` コールバックで生の値を返したり、`/auth/callback` の `next` を自前で転送したりすると、その瞬間に穴が戻ります**。フレームワーク任せにせず、自分で書いた遷移経路は必ず検証してください。

**Q. オープンリダイレクトはWAFで止められますか？**
A. 止められません。`?next=https://evil.example` は認証も書式も正しい「正規リクエスト」で、攻撃か否かは「このアプリがどの遷移先を許すか」というアプリ固有のルールでしか決まらないからです。WAFはあなたの許可ポリシーを知りません。コード側の検証が唯一の防御です。

---

## まとめ：遷移先は「文字列」ではなく「構造」で判断する

要点を整理します。

- オープンリダイレクトは、信頼できる自社ドメインを起点に攻撃者ドメインへ転送させる欠陥。**フィッシングと、認証直後のトークン窃取**の踏み台になる。
- Next.jsでは **`redirect(searchParams.next)`・ログイン後の `callbackUrl`・OAuthコールバックの `next`・middlewareの `redirect`** が典型的な発生箇所。`redirect()` は絶対URLを渡せば外部にも飛ぶ。
- **素朴な文字列チェックは破られる**——`//evil`（プロトコル相対）、`/\evil`（円記号）、`yourapp.com@evil`（userinfo）、`yourapp.com.evil`（サブドメイン詐称）。遷移先は文字列ではなく**構造（origin/host/scheme）**で判断する。
- 防御の既定は「**`new URL` で解決しオリジン照合した相対パスのみ許可**」。外部遷移は **host完全一致のallowlist＋https固定**で例外的に許す。返す値はパス成分だけに削る。
- **汚染入力→redirectシンクのデータフローはtaint解析で機械的に検出できる**（`npx @aegiskit/cli scan`）。ただし**許可先の設計は人間の判断**であり、ツールは検出を助けるだけで正しさは証明しない。

AIで速く作ること自体は正しい。**速く作ったものを、漏らさず安全に固める**——その仕組みづくりや、既存のNext.jsアプリの認証フロー・リダイレクト設計のレビューが必要であれば、お気軽にご相談ください。

---

## 参考資料

- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [OWASP Web Security Testing Guide（リダイレクト検査の指針）](https://owasp.org/www-project-web-security-testing-guide/)
- [OWASP Application Security Verification Standard（ASVS／許可先のallowlist検証要件）](https://owasp.org/www-project-application-security-verification-standard/)
