Skip to content

JavaScript URL コンストラクターのネットワークパス参照によるオープンリダイレクト脆弱性と3つの対策パターン

JavaScript の URL コンストラクターを使用したリダイレクト処理には、実装方法によってはオープンリダイレクト脆弱性を作ってしまう可能性があります。この記事では、URL コンストラクターと RFC について調査したので、その内容をまとめます。

  • オープンリダイレクト脆弱性の仕組み
  • JavaScript URL コンストラクターの危険な挙動
  • 安全なリダイレクト処理の実装方法

https://github.com/kntks/blog-code/tree/main/2025/08/javascript-url-constructor-open-redirect

業務で実装したいリダイレクト処理に関するコードレビューをしました。そのコードは JavaScript の URL コンストラクターを使用したリダイレクト URL 取得処理でした。

一部コードを書き換えていますが、簡単に書くと以下のようなコードでした。

function extractCallbackUrl(nextUrl: URL): string | null {
const callbackUrl = nextUrl.searchParams.get("callbackUrl");
if (!callbackUrl) return null;
const decodedCallbackUrl = decodeURIComponent(callbackUrl);
try {
const url = new URL(decodedCallbackUrl, nextUrl.origin);
return url.origin === nextUrl.origin ? decodedCallbackUrl : null;
} catch {
return null;
}
}

TypeScript plaground

しかし、このコードは攻撃者が外部サイトへリダイレクトできる脆弱性を含んでいる可能性があるではないかと感じ、オープンリダイレクトについて調査しました。

今回は、URL コンストラクターの挙動と、オープンリダイレクト脆弱性のリスク、その対策について学んだことを記録します。

実装したいリダイレクト処理は、認証済みユーザーのみがアクセスできる保護されたページへのリダイレクトです。未認証ユーザーはログインページにリダイレクトされ、ログイン後に元の保護されたページに戻る仕様です。

  • 保護されたページhttps://myapp.com/protected
  • アクセス制御:認証済みユーザーのみアクセス可能
  • リダイレクト動作:未認証ユーザーはログインページへリダイレクト
  • ログイン後の動作:元の保護されたページにリダイレクト

オープンリダイレクト脆弱性とは

Section titled “オープンリダイレクト脆弱性とは”

攻撃者が任意の外部サイトへユーザーをリダイレクトできる脆弱性のことです

参考:Unvalidated Redirects and Forwards Cheat Sheet - OWASP

  1. 攻撃者が悪意のある URL を作成
  2. ユーザーが信頼できるドメインの URL と誤認してアクセス
  3. アプリケーションが外部サイトにリダイレクト
  4. フィッシングサイトなどでユーザーが被害を受ける
  • フィッシング攻撃:偽のログインページで認証情報を窃取
  • マルウェア配布:悪意のあるファイルのダウンロードサイトに誘導
  • 信頼の悪用:正規サイトのドメインを利用して攻撃の信頼性を向上

参考:CWE-601: URL Redirection to Untrusted Site (‘Open Redirect’)

オープンリダイレクト自体が直接的な被害をもたらすことは少ないですが、他の攻撃と組み合わせることで、ユーザーの信頼を悪用し、フィッシングやマルウェア配布などの被害を引き起こす可能性があります。

This vulnerability could be used as part of a phishing scam by redirecting users to a malicious sit

訳:この脆弱性は、ユーザーを悪意のあるサイトにリダイレクトさせることで、フィッシング詐欺の一部として利用される可能性があります。

引用:Unvalidated Redirects and Forwards Cheat Sheet - OWASP

JavaScript URL コンストラクターの危険性

Section titled “JavaScript URL コンストラクターの危険性”
function buildUrlAndConsole(url: string) {
const base = "http://myapp.com:3000"
const u = new URL(url, base)
console.log({
host: u.host,
origin: u.origin,
pathname: u.pathname,
href: u.href
})
}
/**
* 意図した入力
*/
buildUrlAndConsole("/protected")
// {
// "host": "myapp.com:3000",
// "origin": "http://myapp.com:3000",
// "pathname": "/protected",
// "href": "http://myapp.com:3000/protected"
// }
/**
* 意図しない入力
*/
buildUrlAndConsole("//evil.com")
// {
// "host": "evil.com",
// "origin": "http://evil.com",
// "pathname": "/",
// "href": "http://evil.com/"
// }

TypeScript playground

以下の挙動により危険性が生じます。

  • //evil.com という URL は現在のプロトコルを継承して完全な URL になる
  • URL コンストラクターの第 2 引数(base URL)が無視される
  • 外部サイトへのリダイレクトが発生する

URL コンストラクターがなぜこのような挙動なのか調査してみると、JavaScript の URL コンストラクタは 相対参照の URL への解決 をサポートしており、このドキュメントは RFC 3986 の 5.2. Relative Resolution を関連情報として紹介しています。 ここから、RFC 3986 の仕様に基づいて、// で始まるネットワークパス参照がどのように解決されるかを見ていきます。

// から始まるパスについて、RFC 3986 では以下のように定義されています。

A relative reference that begins with two slash characters is termed a network-path reference; such references are rarely used.

訳:2 つのスラッシュ文字で始まる相対参照は、ネットワークパス参照と呼ばれます。このような参照はほとんど使用されません。

引用:4.2. Relative Reference - RFC 3986

URI の汎用的な構文は、スキーム(scheme)、オーソリティ(authority)、パス(path)、クエリ(query)、フラグメント(fragment)と呼ばれる階層的なコンポーネントの並びで構成されます。

RFC では例として foo://example.com:8042/over/there?name=ferret#nose を以下のように説明しています。

fooexample.com:8042/over/therename=ferretnose
部分スキームオーソリティパスクエリフラグメント
説明プロトコルを指定ホスト名とポート番号リソースの場所パラメータ文書内の特定の部分

参考:3. Syntax Components - RFC 3986

URI の構成の話から、オーソリティ(authority)コンポーネントについて見ていきます。

RFC 3986 では、// で始まる文字列をオーソリティとして扱うようです。

The authority component is preceded by a double slash (”//”) and is terminated by the next slash (”/”), question mark (”?”), or number sign (”#”) character, or by the end of the URI.

訳:authorityコンポーネントは二重スラッシュ(”//“)に先行され、次のスラッシュ(”/”)、クエスチョンマーク(”?”)、またはナンバーサイン(”#“)文字、あるいはURIの終端によって終了される。

引用:3.2. Authority - RFC 3986

RFC 3986 5.2.2 にある擬似コードを見てみます。

このとき、//evil.com のようなネットワークパス参照を解決する際、どこでスキームを決定するかが重要です。

  • R: 参照 URI
  • Base: 基準 URI
  • T: 結果 URI
# ...省略
if defined(R.scheme) then
# ...省略
else
# ============================================================
# スキームが未定義の場合はこのブロックに入る
# ============================================================
if defined(R.authority) then
# ============================================================
# "//" で始まる場合はこのブロックに入る
# ==============================================================
T.authority = R.authority; # オーソリティを R から取得
T.path = remove_dot_segments(R.path); # パスの正規化
T.query = R.query; # クエリも R から
else
# ...省略
endif;
# ============================================================
# Base のスキームを使用する
# ============================================================
T.scheme = Base.scheme; # スキームは Base から
endif;
# ...省略

この擬似コードから、//evil.com のようなネットワークパス参照は、スキームが未定義であるため、基準 URI のスキームを使用して解決されることがわかります。つまり、//evil.com は現在のプロトコル(HTTP または HTTPS)を継承し、外部サイトへのリダイレクトが可能になります。

const base = "http://myapp.com:3000"
const url = "//evil.com"
const u = new URL(url, base)
console.log({
host: u.host,
origin: u.origin,
pathname: u.pathname,
href: u.href
})
// {
// "host": "evil.com",
// "origin": "http://evil.com",
// "pathname": "/",
// "href": "http://evil.com/"
// }

TypeScript playground

(他にも実装方法があると思いますが)認証後のリダイレクト機能を実装する際の 3 つのアプローチを提案します。

方法メリットデメリットセキュリティレベル
クエリストリング + バリデーション実装が簡単、ステートレスURL が長くなる、改ざんリスク
JWS による Cookie 保存改ざん検知可能、URL がクリーン実装が複雑、トークン管理必要
サーバー側 KVS 保存最高のセキュリティ、改ざん不可インフラが複雑、状態管理必要最高

今回はチームの方針により クエリストリング方式 を採用します。

クエリストリング方式では、受け取る URL が絶対パスか相対パスかを検証し、外部ドメインへのリダイレクトを防ぐ必要があります。

OWASP の推奨事項に基づく 3 つの対策を実装します。

  1. ドメイン検証による対策:同一ドメインのみ許可
  2. 相対パス限定による対策:絶対パスを相対パスに変換
  3. 許可リスト制御による対策:事前定義されたパスのみ許可

以下の 3 つの関数を実装し、それぞれの特性を比較します:

関数名アプローチセキュリティレベル運用コスト適用場面
extractCallbackUrlドメイン検証基本的な保護が必要な場合
extractRelativeCallbackUrl相対パス限定中〜高シンプルな構成のアプリ
extractAllowedCallbackUrl許可リスト制御厳密な制御が必要な場合

1. ドメイン検証方式(extractCallbackUrl

  • メリット:実装が簡単、新しいパスの追加時に変更不要
  • デメリット:同一ドメイン内の全パスにアクセス可能、ネットワークパスの攻撃のリスク

2. 相対パス限定方式(extractRelativeCallbackUrl

  • メリット:外部ドメイン攻撃を完全にブロック、運用コストが低い
  • デメリット:同一ドメイン内の意図しないパスへのアクセスは防げない

3. 許可リスト方式(extractAllowedCallbackUrl

  • メリット:意図しないパスへのアクセスを完全に防止
  • デメリット:新しいパス追加時にコード変更が必要、運用コストが高い

コードとテストを生成AIに依頼し、ある程度防御できるコードにします。

項目バージョン
MacVentura 15.5
Node.js24.1.0
Vitest3.2.4
Terminal window
# プロジェクト初期化
pnpm init
# 必要なパッケージのインストール
pnpm add -D tsx typescript @types/node vitest
# TypeScript 設定ファイルの作成
pnpm tsc --init

以下の3つの関数を実装します。

  • extractCallbackUrl
  • extractRelativeCallbackUrl
  • extractAllowedCallbackUrl
    • 許可されたパス(絶対、相対含む)のみ

実際のコードは、src/main.ts - GitHub にあります。
ここでは重要な部分のみを抜粋します。

src/main.ts
export function extractCallbackUrl(nextUrl: URL): string | null {
const callbackUrl = nextUrl.searchParams.get("callbackUrl")
if (!callbackUrl) return null;
// ...省略...
const decodedCallbackUrl = decodeURIComponent(callbackUrl)
// ...省略...
// ネットワークパス(プロトコル相対URL)攻撃の防止(//で始まる)
if (decodedCallbackUrl.startsWith("//")) {
return null;
}
// 不正なプロトコル開始パターンの防止
if (decodedCallbackUrl.startsWith("://")) {
return null;
}
// ...省略...
}

一部省略していますが、攻撃パターンを含むテストコードを以下に示します。

src/main.test.ts
describe('extractCallbackUrl', () => {
// ...省略...
// Open Redirect攻撃パターンのテストケース
test.each([
['//evil.com/malicious', 'ネットワークパス(プロトコル相対URL)攻撃'],
['///evil.com/malicious', 'トリプルスラッシュ攻撃'],
['\\\\evil.com\\malicious', 'バックスラッシュエスケープ攻撃'],
['/\\evil.com/malicious', 'スラッシュ+バックスラッシュ攻撃'],
// ...省略...
])
})

https://github.com/kntks/blog-code/blob/main/2025/08/javascript-url-constructor-open-redirect/src/main.test.ts

Terminal window
$ pnpm test:run
# ...省略
Test Files 1 passed (1)
Tests 204 passed (204)
  • URL コンストラクターの制限:ネットワークパスは外部ドメインを指す可能性がある
  • 入力値検証の必要性:リダイレクト先 URL は必ず検証する
  • 最小権限原則:必要最小限のリダイレクト先のみ許可する
  1. ドメイン厳密検証を実装する
  2. 許可リスト方式を採用する
  3. 相対パス限定の設計にする