起動インテントの deep‑link 引数をサニタイズして開始ロケーションを保護

hotwired/hotwire-native-android

Google の Navigation ライブラリが外部から指定可能な deepLinkArgsdeepLinkExtras 上に上書きできるため、攻撃者が任意の URL を WebView に読み込ませる脆弱性がありました。今回の PR は NavigatorHost が起動インテント全体をサニタイズし、開始ロケーションが常にアプリ設定のホストに限定されるように修正しています。

背景

NavigatorHost.ensureDeeplinkStartLocationValid() は従来、DEEPLINK_EXTRAS_KEY に格納された location だけを検証していましたが、NavController は同時に DEEPLINK_ARGS_KEY のバンドルも取得し、後からマージすることで deepLinkExtras を上書きします。これにより外部から送られた deepLinkArgs が検証済みの URL を置き換え、任意ページへの遷移が可能となっていました。

この問題は、起動インテントの callingPackageActivity.referrer が外部から操作可能で、信頼できる判定基準が存在しないことが根本原因です。そのため、外部か内部かを判別して条件分岐することは実装上不可能でした。

本セクションは、外部インテントが deepLinkArgs を介して検証バイパスを行える点を示し、なぜ全体サニタイズが唯一の解決策になるかを説明します。

技術的な変更

DEEPLINK_ARGS_KEY を新たに定義し、起動インテントの deepLinkArgs バンドルを全て空の Bundle に置き換えることで上書きを防止します。加えて、DEEPLINK_EXTRAS_KEYlocation が設定された開始ホストと一致しない場合は、構成で定義された startLocation に書き換えます。

@@ -15,6 +15,7 @@ import dev.hotwire.navigation.activities.HotwireActivity
 import dev.hotwire.navigation.config.HotwireNavigation

 internal const val DEEPLINK_EXTRAS_KEY = "android-support-nav:controller:deepLinkExtras"
+internal const val DEEPLINK_ARGS_KEY = "android-support-nav:controller:deepLinkArgs"
 internal const val LOCATION_KEY = "location"
@@
-        val extrasBundle = activity.intent.extras?.getBundle(DEEPLINK_EXTRAS_KEY) ?: return
+        val intent = activity.intent
+
+        // deepLinkArgs は deepLinkExtras を上書きできるため、各バンドルを空にして除去する
+        intent.extras?.getParcelableArrayList<Bundle>(DEEPLINK_ARGS_KEY)?.let { args ->
+            intent.putParcelableArrayListExtra(DEEPLINK_ARGS_KEY, ArrayList(args.map { Bundle() }))
+        }
+
+        val extrasBundle = intent.extras?.getBundle(DEEPLINK_EXTRAS_KEY) ?: return
         val startLocation = extrasBundle.getString(LOCATION_KEY) ?: return

         val deepLinkStartUri = startLocation.toUri()
         val configStartUri = configuration.startLocation.toUri()
@@
-            activity.intent.putExtra(DEEPLINK_EXTRAS_KEY, extrasBundle)
+            intent.putExtra(DEEPLINK_EXTRAS_KEY, extrasBundle)
         }

テストコードも拡張され、deepLinkArgs が空になること、ホストが一致しない場合に location が設定値に戻ること、同一ホストの場合は保持されることを検証しています。

この実装は Intent 全体に対して無条件でサニタイズを行うため、正しい同一ホストの deep‑link はそのまま機能し、外部からの任意 URL 注入は防げます。

設計判断

サニタイズを「条件付き」ではなく 無条件 に実施する方針が採られました。callingPackageActivity.referrer は攻撃者が設定可能で判別できないため、信頼できるインテントかどうかを判定できません。したがって、全ての起動インテントで deepLinkArgs をクリアしつつ、deepLinkExtraslocation のみをホスト検証後に残すというシンプルな手法が選択されました。

このアプローチは、既存の deepLinkExtras 検証ロジックを極力変更せずに、後続の上書き だけを遮断します。代替案としては新しいキー deepLinkArgs を除外する、またはフラグで制御するといった手段が考えられましたが、キーの追加は API 互換性のリスクを伴い、フラグ制御は判定ロジックが増えて保守コストが上がります。最小限のコード変更でセキュリティを確保できた点が評価されました。

まとめ

本 PR は NavigatorHost が起動インテントの deepLinkArgs を空にし、deepLinkExtraslocation が設定ホストと合致しなければ構成値に置換えることで、外部からの任意 URL 注入を防止します。無条件サニタイズという設計判断により、正当な同一ホスト deep‑link はそのまま機能し、セキュリティと既存動作の両立が実現されました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
7116bb82

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文 → 背景 → 技術的な変更 → 設計判断 → まとめ の流れが明確で、全て必須要素が揃っています。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックは問題なくハイライトされていますが、PRリンクが "[PR #200](…)" となっており、仕様の "[#123](…)" 形式と完全に一致していません。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門エンジニア向けの技術的記述で、過度な初心者向け解説はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論→各論→結論の構成で、段落はトピックセンテンスで始まり 1 段落1トピック、長さも適切です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内の diff ブロックは PR の diff 内容と一致しており、変更点が正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

deepLinkArgs、deepLinkExtras、NavigatorHost などの用語は PR と一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的な説明は PR の記述と矛盾せず、因果関係も論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事のすべての主張は PR タイトル・説明・Diff で裏付けられており、ハルシネーションはありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR 番号 #200 など数値は正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルは PR の趣旨(deep‑link 引数の検証と保護)を適切に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

外部のバージョンサポート情報やリリース日程など、PR に無い外部知識は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の歪曲はなく、PR の記述と一致しています。