起動インテントの deep‑link 引数をサニタイズして開始ロケーションを保護
Google の Navigation ライブラリが外部から指定可能な deepLinkArgs を deepLinkExtras 上に上書きできるため、攻撃者が任意の URL を WebView に読み込ませる脆弱性がありました。今回の PR は NavigatorHost が起動インテント全体をサニタイズし、開始ロケーションが常にアプリ設定のホストに限定されるように修正しています。
背景
NavigatorHost.ensureDeeplinkStartLocationValid() は従来、DEEPLINK_EXTRAS_KEY に格納された location だけを検証していましたが、NavController は同時に DEEPLINK_ARGS_KEY のバンドルも取得し、後からマージすることで deepLinkExtras を上書きします。これにより外部から送られた deepLinkArgs が検証済みの URL を置き換え、任意ページへの遷移が可能となっていました。
この問題は、起動インテントの callingPackage や Activity.referrer が外部から操作可能で、信頼できる判定基準が存在しないことが根本原因です。そのため、外部か内部かを判別して条件分岐することは実装上不可能でした。
本セクションは、外部インテントが deepLinkArgs を介して検証バイパスを行える点を示し、なぜ全体サニタイズが唯一の解決策になるかを説明します。
技術的な変更
DEEPLINK_ARGS_KEY を新たに定義し、起動インテントの deepLinkArgs バンドルを全て空の Bundle に置き換えることで上書きを防止します。加えて、DEEPLINK_EXTRAS_KEY の location が設定された開始ホストと一致しない場合は、構成で定義された 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 注入は防げます。
設計判断
サニタイズを「条件付き」ではなく 無条件 に実施する方針が採られました。callingPackage や Activity.referrer は攻撃者が設定可能で判別できないため、信頼できるインテントかどうかを判定できません。したがって、全ての起動インテントで deepLinkArgs をクリアしつつ、deepLinkExtras の location のみをホスト検証後に残すというシンプルな手法が選択されました。
このアプローチは、既存の deepLinkExtras 検証ロジックを極力変更せずに、後続の上書き だけを遮断します。代替案としては新しいキー deepLinkArgs を除外する、またはフラグで制御するといった手段が考えられましたが、キーの追加は API 互換性のリスクを伴い、フラグ制御は判定ロジックが増えて保守コストが上がります。最小限のコード変更でセキュリティを確保できた点が評価されました。
まとめ
本 PR は NavigatorHost が起動インテントの deepLinkArgs を空にし、deepLinkExtras の location が設定ホストと合致しなければ構成値に置換えることで、外部からの任意 URL 注入を防止します。無条件サニタイズという設計判断により、正当な同一ホスト deep‑link はそのまま機能し、セキュリティと既存動作の両立が実現されました。