JavaScript URL コンストラクターのネットワークパス参照によるオープンリダイレクト脆弱性と3つの対策パターン
JavaScript の URL コンストラクターを使用したリダイレクト処理には、実装方法によってはオープンリダイレクト脆弱性を作ってしまう可能性があります。この記事では、URL コンストラクターと RFC について調査したので、その内容をまとめます。
- オープンリダイレクト脆弱性の仕組み
- JavaScript URL コンストラクターの危険な挙動
- 安全なリダイレクト処理の実装方法
https://github.com/kntks/blog-code/tree/main/2025/08/javascript-url-constructor-open-redirect
発見のきっかけ
Section titled “発見のきっかけ”業務で実装したいリダイレクト処理に関するコードレビューをしました。そのコードは 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; }}
しかし、このコードは攻撃者が外部サイトへリダイレクトできる脆弱性を含んでいる可能性があるではないかと感じ、オープンリダイレクトについて調査しました。
今回は、URL コンストラクターの挙動と、オープンリダイレクト脆弱性のリスク、その対策について学んだことを記録します。
想定アプリケーション仕様
Section titled “想定アプリケーション仕様”実装したいリダイレクト処理は、認証済みユーザーのみがアクセスできる保護されたページへのリダイレクトです。未認証ユーザーはログインページにリダイレクトされ、ログイン後に元の保護されたページに戻る仕様です。
- 保護されたページ:
https://myapp.com/protected
- アクセス制御:認証済みユーザーのみアクセス可能
- リダイレクト動作:未認証ユーザーはログインページへリダイレクト
- ログイン後の動作:元の保護されたページにリダイレクト
システム動作フロー
Section titled “システム動作フロー”脆弱性の理解
Section titled “脆弱性の理解”オープンリダイレクト脆弱性とは
Section titled “オープンリダイレクト脆弱性とは”攻撃者が任意の外部サイトへユーザーをリダイレクトできる脆弱性のことです
参考:Unvalidated Redirects and Forwards Cheat Sheet - OWASP
- 攻撃者が悪意のある URL を作成
- ユーザーが信頼できるドメインの URL と誤認してアクセス
- アプリケーションが外部サイトにリダイレクト
- フィッシングサイトなどでユーザーが被害を受ける
- フィッシング攻撃:偽のログインページで認証情報を窃取
- マルウェア配布:悪意のあるファイルのダウンロードサイトに誘導
- 信頼の悪用:正規サイトのドメインを利用して攻撃の信頼性を向上
参考: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 コンストラクターの危険性”問題のあるコード例
Section titled “問題のあるコード例”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/"// }
以下の挙動により危険性が生じます。
//evil.com
という URL は現在のプロトコルを継承して完全な URL になる- URL コンストラクターの第 2 引数(base URL)が無視される
- 外部サイトへのリダイレクトが発生する
RFC 3986 に基づく仕様の理解
Section titled “RFC 3986 に基づく仕様の理解”ネットワークパス参照
Section titled “ネットワークパス参照”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 の構成要素
Section titled “URI の構成要素”URI の汎用的な構文は、スキーム(scheme)、オーソリティ(authority)、パス(path)、クエリ(query)、フラグメント(fragment)と呼ばれる階層的なコンポーネントの並びで構成されます。
RFC では例として foo://example.com:8042/over/there?name=ferret#nose
を以下のように説明しています。
値 | foo | example.com:8042 | /over/there | name=ferret | nose |
---|---|---|---|---|---|
部分 | スキーム | オーソリティ | パス | クエリ | フラグメント |
説明 | プロトコルを指定 | ホスト名とポート番号 | リソースの場所 | パラメータ | 文書内の特定の部分 |
参考:3. Syntax Components - RFC 3986
Authority
Section titled “Authority”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の終端によって終了される。
reference の変換
Section titled “reference の変換”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/"// }
リダイレクト機能の実現案
Section titled “リダイレクト機能の実現案”実現方法の比較
Section titled “実現方法の比較”(他にも実装方法があると思いますが)認証後のリダイレクト機能を実装する際の 3 つのアプローチを提案します。
方法 | メリット | デメリット | セキュリティレベル |
---|---|---|---|
クエリストリング + バリデーション | 実装が簡単、ステートレス | URL が長くなる、改ざんリスク | 中 |
JWS による Cookie 保存 | 改ざん検知可能、URL がクリーン | 実装が複雑、トークン管理必要 | 高 |
サーバー側 KVS 保存 | 最高のセキュリティ、改ざん不可 | インフラが複雑、状態管理必要 | 最高 |
今回はチームの方針により クエリストリング方式 を採用します。
バリデーション戦略
Section titled “バリデーション戦略”クエリストリング方式では、受け取る URL が絶対パスか相対パスかを検証し、外部ドメインへのリダイレクトを防ぐ必要があります。
OWASP の推奨事項に基づく 3 つの対策を実装します。
- ドメイン検証による対策:同一ドメインのみ許可
- 相対パス限定による対策:絶対パスを相対パスに変換
- 許可リスト制御による対策:事前定義されたパスのみ許可
バリデーション戦略の比較
Section titled “バリデーション戦略の比較”以下の 3 つの関数を実装し、それぞれの特性を比較します:
関数名 | アプローチ | セキュリティレベル | 運用コスト | 適用場面 |
---|---|---|---|---|
extractCallbackUrl | ドメイン検証 | 中 | 低 | 基本的な保護が必要な場合 |
extractRelativeCallbackUrl | 相対パス限定 | 中〜高 | 低 | シンプルな構成のアプリ |
extractAllowedCallbackUrl | 許可リスト制御 | 高 | 高 | 厳密な制御が必要な場合 |
実装方式別の詳細比較
Section titled “実装方式別の詳細比較”1. ドメイン検証方式(extractCallbackUrl
)
- ✅ メリット:実装が簡単、新しいパスの追加時に変更不要
- ❌ デメリット:同一ドメイン内の全パスにアクセス可能、ネットワークパスの攻撃のリスク
2. 相対パス限定方式(extractRelativeCallbackUrl
)
- ✅ メリット:外部ドメイン攻撃を完全にブロック、運用コストが低い
- ❌ デメリット:同一ドメイン内の意図しないパスへのアクセスは防げない
3. 許可リスト方式(extractAllowedCallbackUrl
)
- ✅ メリット:意図しないパスへのアクセスを完全に防止
- ❌ デメリット:新しいパス追加時にコード変更が必要、運用コストが高い
検証とテスト
Section titled “検証とテスト”テスト環境構築
Section titled “テスト環境構築”コードとテストを生成AIに依頼し、ある程度防御できるコードにします。
項目 | バージョン |
---|---|
Mac | Ventura 15.5 |
Node.js | 24.1.0 |
Vitest | 3.2.4 |
セットアップ手順
Section titled “セットアップ手順”# プロジェクト初期化pnpm init
# 必要なパッケージのインストールpnpm add -D tsx typescript @types/node vitest
# TypeScript 設定ファイルの作成pnpm tsc --init
以下の3つの関数を実装します。
- extractCallbackUrl
- extractRelativeCallbackUrl
- extractAllowedCallbackUrl
- 許可されたパス(絶対、相対含む)のみ
実際のコードは、src/main.ts - GitHub にあります。
ここでは重要な部分のみを抜粋します。
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; } // ...省略...}
テストコード
Section titled “テストコード”一部省略していますが、攻撃パターンを含むテストコードを以下に示します。
describe('extractCallbackUrl', () => { // ...省略... // Open Redirect攻撃パターンのテストケース test.each([ ['//evil.com/malicious', 'ネットワークパス(プロトコル相対URL)攻撃'], ['///evil.com/malicious', 'トリプルスラッシュ攻撃'], ['\\\\evil.com\\malicious', 'バックスラッシュエスケープ攻撃'], ['/\\evil.com/malicious', 'スラッシュ+バックスラッシュ攻撃'], // ...省略... ])})
テスト実行結果
Section titled “テスト実行結果”$ pnpm test:run
# ...省略
Test Files 1 passed (1) Tests 204 passed (204)
重要ポイント
Section titled “重要ポイント”- URL コンストラクターの制限:ネットワークパスは外部ドメインを指す可能性がある
- 入力値検証の必要性:リダイレクト先 URL は必ず検証する
- 最小権限原則:必要最小限のリダイレクト先のみ許可する
- ドメイン厳密検証を実装する
- 許可リスト方式を採用する
- 相対パス限定の設計にする