From 2e78a1cb0658b5af2537bf667b2b59b7c47c00eb Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Tue, 21 Jul 2026 17:28:54 -0700 Subject: [PATCH 1/6] First pass --- .../stricter-intent-extra-verification.md | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 sites/docs/src/content/release/breaking-changes/stricter-intent-extra-verification.md diff --git a/sites/docs/src/content/release/breaking-changes/stricter-intent-extra-verification.md b/sites/docs/src/content/release/breaking-changes/stricter-intent-extra-verification.md new file mode 100644 index 0000000000..4ae20ffe69 --- /dev/null +++ b/sites/docs/src/content/release/breaking-changes/stricter-intent-extra-verification.md @@ -0,0 +1,98 @@ +--- +title: Stricter Intent Extra Verification for FlutterActivity +description: Android Intent extras for routing and entrypoints are now strictly verified. +--- + +{% render "docs/breaking-changes.md" %} + +## Summary + +To protect against Intent-based vulnerability exploits, +the Flutter Android embedder (`FlutterActivity` and `FlutterFragmentActivity`) +now strictly verifies the sender of Intents +before processing `EXTRA_INITIAL_ROUTE`, `EXTRA_DART_ENTRYPOINT`, +and `EXTRA_DART_ENTRYPOINT_ARGS`. + +External intents that try to pass these string extras (e.g., `"route"`) +will now be ignored +unless they are verified as originating from the app itself. + +## Context + +Historically, the Flutter Android embedder unconditionally accepted +routing and entrypoint configuration from Intent extras. +While convenient, this allowed third-party applications on the same device +to maliciously inject entrypoints or routes into a Flutter app +by sending an explicit Intent. + +To mitigate arbitrary route injection (CWE-940) +and entrypoint injection (CWE-926), +we introduced a strict verification layer +using Android's `getLaunchedFromUid()` (on Android 14+) +and `getCallingPackage()` (on Android 13 and below). + +If the sender of the Intent cannot be verified as the app itself, +the intent extras will be safely ignored. +Standard Deep Links (`intent.getData()`) remain supported +but are now validated against the app's declared `AndroidManifest.xml` +intent-filters. + +## Description of change + +Any external system passing the `"route"`, `"dart_entrypoint"`, +or `"dart_entrypoint_args"` extras will no longer have an effect. + +The most common legitimate use-case affected by this change +is **Push Notifications** (e.g., Firebase Cloud Messaging). +Push notification SDKs frequently build a `PendingIntent` +that includes a `"route"` string extra +to navigate the user to a specific screen when the notification is tapped. + +On Android 14+ (API 34), push notifications via `PendingIntent` +will generally continue to work +because `getLaunchedFromUid()` can successfully verify +the identity of the `PendingIntent` creator. + +However, on **Android 13 and below (API 33 and lower)**, +`getCallingPackage()` is often `null` +when a `PendingIntent` is fired from the notification tray. +As a result, the embedder will safely assume the intent is untrusted +and **ignore the route extra**. + +## Migration guide + +If your application relies on Push Notifications or App Shortcuts +to launch the app into a specific route, +you should migrate to using standard Deep Link URIs +instead of Intent extras. + +**Before (Deprecated & Insecure):** +```java +Intent intent = new Intent(context, MainActivity.class); +intent.putExtra("route", "/product_details"); +``` + +**After (Recommended):** +```java +Intent intent = new Intent(context, MainActivity.class); +intent.setAction(Intent.ACTION_VIEW); +intent.setData(Uri.parse("myapp://product_details")); +``` + +Ensure that your `AndroidManifest.xml` declares the corresponding +`` for the deep link, +as the embedder will query the PackageManager +to verify the route is legitimate. + +Alternatively, if you must use Intent extras, +configure your push notification to launch a custom `Activity` +or `BroadcastReceiver` that you control. +Inside that component, construct a new explicit Intent +to launch your `FlutterActivity`. +Because the new Intent is launched from within your app, +the embedder will successfully verify it as self-sent. + +## Timeline + +Landed in version: TBD
+In stable release: TBD From 23095ca4e4d443f6654496c03ae1c78e78d71c66 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Wed, 5 Aug 2026 15:46:14 -0700 Subject: [PATCH 2/6] ag updates --- ...-android-entrypoint-intent-verification.md | 136 ++++++++++++++++++ .../stricter-intent-extra-verification.md | 98 ------------- 2 files changed, 136 insertions(+), 98 deletions(-) create mode 100644 sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md delete mode 100644 sites/docs/src/content/release/breaking-changes/stricter-intent-extra-verification.md diff --git a/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md b/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md new file mode 100644 index 0000000000..4e428afb5b --- /dev/null +++ b/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md @@ -0,0 +1,136 @@ +--- +title: Stricter Android Intent Verification for App Entrypoints +description: Android Intent extras for routing, entrypoints, and cached engines are now strictly verified. +--- + +{% render "docs/breaking-changes.md" %} + +## Summary + +To protect against Intent-based vulnerability exploits, +the Flutter Android embedder (`FlutterActivity` and `FlutterFragmentActivity`) +now strictly verifies the sender of Intents +before processing `EXTRA_INITIAL_ROUTE`, `EXTRA_DART_ENTRYPOINT`, +`EXTRA_DART_ENTRYPOINT_ARGS`, `EXTRA_CACHED_ENGINE_ID`, and +`EXTRA_CACHED_ENGINE_GROUP_ID`. + +External intents that try to pass these extras (such as `"route"`, +`"dart_entrypoint"`, or `"cached_engine_id"`) will now be ignored +unless they are verified as originating from the app itself. + +## Context + +Historically, the Flutter Android embedder unconditionally accepted +routing, entrypoint, and engine configuration from Intent extras. +While convenient, this allowed third-party applications on the same device +to maliciously inject parameters into a Flutter app. + +This exposed applications to security vulnerabilities, including: +1. **Route Hijacking (CWE-940):** Forcing the app to load a sensitive internal route (via `EXTRA_INITIAL_ROUTE`). +2. **Entrypoint Injection (CWE-926):** Forcing the app to execute arbitrary Dart functions or passing unsafe parameters (via `EXTRA_DART_ENTRYPOINT` and `EXTRA_DART_ENTRYPOINT_ARGS`). +3. **Session Hijacking / Privilege Escalation:** Reusing an active, pre-authenticated in-memory `FlutterEngine` instance (via `EXTRA_CACHED_ENGINE_ID` and `EXTRA_CACHED_ENGINE_GROUP_ID`) to access user-restricted states. + +To mitigate these issues, we introduced a strict verification layer using Android's `getLaunchedFromUid()` (on Android 14+) and `getCallingPackage()` (on Android 13 and below). + +If the sender of the Intent cannot be verified as the app itself, the intent extras will be safely ignored. Standard Deep Links (`intent.getData()`) remain supported but are now validated against the app's declared `AndroidManifest.xml` intent-filters. + +## Description of change + +Any external system passing the `"route"`, `"dart_entrypoint"`, `"dart_entrypoint_args"`, `"cached_engine_id"`, or `"cached_engine_group_id"` extras will no longer have an effect in release builds. + +### Behavior in Debug/Profile vs. Release +* **Debug and Profile builds:** The verification is bypassed unconditionally to preserve developer velocity, CLI testing (`adb shell am start`), and automated instrumentation tests. However, if a verification check would have failed, a warning is printed to Logcat/console to alert developers. +* **Release builds:** Enforced strictly. Extras from unverified sources are silently ignored. + +### Affected Scenarios +* **Push Notifications:** Push notification SDKs frequently build a `PendingIntent` that includes a `"route"` string extra to navigate the user to a specific screen when the notification is tapped. On Android 14+ (API 34), this will continue to work. On Android 13 and below (API 33 and lower), the route extra will be ignored because the OS calling package metadata is lost. +* **Internal Cached Engine/Entrypoint Launches:** If an application launches a pre-warmed cached engine or custom entrypoint dynamically from its own native code using standard `startActivity(intent)`, it will fail to resolve the cached engine or entrypoint parameters on Android 13 and below if the activity is exported. + +## Migration guide + +Depending on your application's use case, you should migrate using one of the following methods: + +### For Push Notifications and App Shortcuts +We recommend migrating to use standard Deep Link URIs instead of Intent extras. + +**Before (Insecure):** +```java +Intent intent = new Intent(context, MainActivity.class); +intent.putExtra("route", "/product_details"); +``` + +**After (Recommended):** +```java +Intent intent = new Intent(context, MainActivity.class); +intent.setAction(Intent.ACTION_VIEW); +intent.setData(Uri.parse("myapp://product_details")); +``` + +Ensure that your `AndroidManifest.xml` declares the corresponding `` for the deep link, as the embedder will query the PackageManager to verify the route is legitimate. + +Alternatively, if you must use Intent extras, configure your push notification to launch a custom `Activity` or `BroadcastReceiver` that you control (and is not exported). Inside that component, construct a new explicit Intent to launch your `FlutterActivity`. Because the new Intent is launched from within your app, the embedder will successfully verify it as self-sent. + +### For Internal Cached Engine or Entrypoint Launches +If your app launches custom entrypoints or cached engines internally, you have three primary options: + +#### Option 1: Mark host activities as non-exported (Recommended) +If the activity hosting the cached engine or custom entrypoint does not need to be opened by external third-party apps, ensure it is not exported. + +In your `AndroidManifest.xml`: +```xml + + +``` + +#### Option 2: Use `startActivityForResult` for internal launches +If the activity must remain exported (e.g. to handle deep links), but you also want to launch it internally with custom entrypoints, routes, or cached engines on legacy Android versions (API 33 and below): + +Change your Kotlin/Java caller code to request a result (using a dummy request code): +```diff +- startActivity(intent) ++ startActivityForResult(intent, 0) // 0 is a dummy request code +``` +*Note: You do not need to implement `onActivityResult()` in the caller activity.* + +#### Option 3: Configure settings via Manifest `` +For configurations that are static or known at compile time, avoid passing them via Intent extras. Instead, declare them directly in your manifest. + +In your `AndroidManifest.xml`: +```xml + + + + +``` + +#### Option 4: Subclass and programmatically define parameters +For dynamic configurations that cannot use `startActivityForResult` (e.g. launches from background `Services` or `BroadcastReceivers`), you can subclass `FlutterActivity` or `FlutterFragmentActivity` and programmatically supply the parameters: + +```java +public class MyFlutterActivity extends FlutterActivity { + @NonNull + @Override + public String getDartEntrypointFunctionName() { + return MyConfigManager.getDynamicEntrypoint(); + } + + @Nullable + @Override + public List getDartEntrypointArgs() { + return MyConfigManager.getDynamicArgs(); + } +} +``` + +## Timeline + +Landed in version: TBD
+In stable release: TBD diff --git a/sites/docs/src/content/release/breaking-changes/stricter-intent-extra-verification.md b/sites/docs/src/content/release/breaking-changes/stricter-intent-extra-verification.md deleted file mode 100644 index 4ae20ffe69..0000000000 --- a/sites/docs/src/content/release/breaking-changes/stricter-intent-extra-verification.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: Stricter Intent Extra Verification for FlutterActivity -description: Android Intent extras for routing and entrypoints are now strictly verified. ---- - -{% render "docs/breaking-changes.md" %} - -## Summary - -To protect against Intent-based vulnerability exploits, -the Flutter Android embedder (`FlutterActivity` and `FlutterFragmentActivity`) -now strictly verifies the sender of Intents -before processing `EXTRA_INITIAL_ROUTE`, `EXTRA_DART_ENTRYPOINT`, -and `EXTRA_DART_ENTRYPOINT_ARGS`. - -External intents that try to pass these string extras (e.g., `"route"`) -will now be ignored -unless they are verified as originating from the app itself. - -## Context - -Historically, the Flutter Android embedder unconditionally accepted -routing and entrypoint configuration from Intent extras. -While convenient, this allowed third-party applications on the same device -to maliciously inject entrypoints or routes into a Flutter app -by sending an explicit Intent. - -To mitigate arbitrary route injection (CWE-940) -and entrypoint injection (CWE-926), -we introduced a strict verification layer -using Android's `getLaunchedFromUid()` (on Android 14+) -and `getCallingPackage()` (on Android 13 and below). - -If the sender of the Intent cannot be verified as the app itself, -the intent extras will be safely ignored. -Standard Deep Links (`intent.getData()`) remain supported -but are now validated against the app's declared `AndroidManifest.xml` -intent-filters. - -## Description of change - -Any external system passing the `"route"`, `"dart_entrypoint"`, -or `"dart_entrypoint_args"` extras will no longer have an effect. - -The most common legitimate use-case affected by this change -is **Push Notifications** (e.g., Firebase Cloud Messaging). -Push notification SDKs frequently build a `PendingIntent` -that includes a `"route"` string extra -to navigate the user to a specific screen when the notification is tapped. - -On Android 14+ (API 34), push notifications via `PendingIntent` -will generally continue to work -because `getLaunchedFromUid()` can successfully verify -the identity of the `PendingIntent` creator. - -However, on **Android 13 and below (API 33 and lower)**, -`getCallingPackage()` is often `null` -when a `PendingIntent` is fired from the notification tray. -As a result, the embedder will safely assume the intent is untrusted -and **ignore the route extra**. - -## Migration guide - -If your application relies on Push Notifications or App Shortcuts -to launch the app into a specific route, -you should migrate to using standard Deep Link URIs -instead of Intent extras. - -**Before (Deprecated & Insecure):** -```java -Intent intent = new Intent(context, MainActivity.class); -intent.putExtra("route", "/product_details"); -``` - -**After (Recommended):** -```java -Intent intent = new Intent(context, MainActivity.class); -intent.setAction(Intent.ACTION_VIEW); -intent.setData(Uri.parse("myapp://product_details")); -``` - -Ensure that your `AndroidManifest.xml` declares the corresponding -`` for the deep link, -as the embedder will query the PackageManager -to verify the route is legitimate. - -Alternatively, if you must use Intent extras, -configure your push notification to launch a custom `Activity` -or `BroadcastReceiver` that you control. -Inside that component, construct a new explicit Intent -to launch your `FlutterActivity`. -Because the new Intent is launched from within your app, -the embedder will successfully verify it as self-sent. - -## Timeline - -Landed in version: TBD
-In stable release: TBD From d2f099ad0f377a07a1d0758c6e4db7085b87717d Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Fri, 7 Aug 2026 14:05:34 -0700 Subject: [PATCH 3/6] review --- ...-android-entrypoint-intent-verification.md | 85 ++++++++++++++----- 1 file changed, 62 insertions(+), 23 deletions(-) diff --git a/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md b/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md index 4e428afb5b..6750ee191c 100644 --- a/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md +++ b/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md @@ -7,51 +7,52 @@ description: Android Intent extras for routing, entrypoints, and cached engines ## Summary -To protect against Intent-based vulnerability exploits, -the Flutter Android embedder (`FlutterActivity` and `FlutterFragmentActivity`) +To protect against [`Intent`][]-based vulnerability exploits, +the Flutter Android embedder (`FlutterActivity`, `FlutterFragmentActivity`, and `FlutterFragment`) now strictly verifies the sender of Intents before processing `EXTRA_INITIAL_ROUTE`, `EXTRA_DART_ENTRYPOINT`, `EXTRA_DART_ENTRYPOINT_ARGS`, `EXTRA_CACHED_ENGINE_ID`, and `EXTRA_CACHED_ENGINE_GROUP_ID`. -External intents that try to pass these extras (such as `"route"`, +External [`Intent`][]s that try to pass these extras (such as `"route"`, `"dart_entrypoint"`, or `"cached_engine_id"`) will now be ignored unless they are verified as originating from the app itself. ## Context Historically, the Flutter Android embedder unconditionally accepted -routing, entrypoint, and engine configuration from Intent extras. -While convenient, this allowed third-party applications on the same device +routing, entrypoint, and engine configuration from `Intent` extras. +This made Flutter Android apps vulnerable third-party applications on the same device to maliciously inject parameters into a Flutter app. This exposed applications to security vulnerabilities, including: -1. **Route Hijacking (CWE-940):** Forcing the app to load a sensitive internal route (via `EXTRA_INITIAL_ROUTE`). -2. **Entrypoint Injection (CWE-926):** Forcing the app to execute arbitrary Dart functions or passing unsafe parameters (via `EXTRA_DART_ENTRYPOINT` and `EXTRA_DART_ENTRYPOINT_ARGS`). -3. **Session Hijacking / Privilege Escalation:** Reusing an active, pre-authenticated in-memory `FlutterEngine` instance (via `EXTRA_CACHED_ENGINE_ID` and `EXTRA_CACHED_ENGINE_GROUP_ID`) to access user-restricted states. +1. **Route Hijacking:** Forcing the app to load a sensitive internal route (via `EXTRA_INITIAL_ROUTE`). +2. **Entrypoint Injection:** Forcing the app to execute arbitrary Dart functions or passing unsafe parameters (via `EXTRA_DART_ENTRYPOINT` and `EXTRA_DART_ENTRYPOINT_ARGS`). +3. **Session Hijacking/Privilege Escalation:** Reusing an active, pre-authenticated in-memory `FlutterEngine` instance (via `EXTRA_CACHED_ENGINE_ID` and `EXTRA_CACHED_ENGINE_GROUP_ID`) to access user-restricted states. -To mitigate these issues, we introduced a strict verification layer using Android's `getLaunchedFromUid()` (on Android 14+) and `getCallingPackage()` (on Android 13 and below). +To mitigate these issues, we introduced a strict verification check using Android's `getLaunchedFromUid()` (on Android 14+) and `getCallingPackage()` (on Android 13 and below). -If the sender of the Intent cannot be verified as the app itself, the intent extras will be safely ignored. Standard Deep Links (`intent.getData()`) remain supported but are now validated against the app's declared `AndroidManifest.xml` intent-filters. +If the sender of the `Intent` cannot be verified as the app itself, the `Intent` extras will be safely ignored. Standard [deep links][] remain supported but are now validated against the app's declared `AndroidManifest.xml` [intent filters][]. ## Description of change -Any external system passing the `"route"`, `"dart_entrypoint"`, `"dart_entrypoint_args"`, `"cached_engine_id"`, or `"cached_engine_group_id"` extras will no longer have an effect in release builds. +Any **external** system passing the `"route"`, `"dart_entrypoint"`, `"dart_entrypoint_args"`, `"cached_engine_id"`, or `"cached_engine_group_id"` extras will no longer have an effect in release builds. ### Behavior in Debug/Profile vs. Release -* **Debug and Profile builds:** The verification is bypassed unconditionally to preserve developer velocity, CLI testing (`adb shell am start`), and automated instrumentation tests. However, if a verification check would have failed, a warning is printed to Logcat/console to alert developers. +* **Debug and Profile builds:** The verification check is bypassed to preserve developer velocity, CLI testing (`adb shell am start`), and automated instrumentation tests. However, if a verification check would have failed, a warning is logged to alert developers. * **Release builds:** Enforced strictly. Extras from unverified sources are silently ignored. ### Affected Scenarios -* **Push Notifications:** Push notification SDKs frequently build a `PendingIntent` that includes a `"route"` string extra to navigate the user to a specific screen when the notification is tapped. On Android 14+ (API 34), this will continue to work. On Android 13 and below (API 33 and lower), the route extra will be ignored because the OS calling package metadata is lost. -* **Internal Cached Engine/Entrypoint Launches:** If an application launches a pre-warmed cached engine or custom entrypoint dynamically from its own native code using standard `startActivity(intent)`, it will fail to resolve the cached engine or entrypoint parameters on Android 13 and below if the activity is exported. +* **Push Notifications:** If a push notification payload or SDK builds a `PendingIntent` that passes the Flutter `"route"` `Intent` extra (which automatically configures the initial route of the app), this extra will be ignored on Android 13 and below (API 33 and lower) because the OS calling package metadata is lost. On Android 14+ (API 34), this will continue to work. Custom data extras processed manually in Dart are not affected. +* **App Shortcuts and Home Screen Widgets:** Tap actions on widgets or shortcuts that launch `FlutterActivity` directly with route/entrypoint extras will fail verification in release builds because the launch is initiated by the system launcher or system widget host. +* **Internal Cached Engine & Entrypoint Launches:** If an application launches a pre-warmed cached engine or custom entrypoint dynamically from its own native code (e.g. from a background [`Service`][] or [`BroadcastReceiver`][] using standard `startActivity(intent)`), it will fail to resolve the cached engine or entrypoint parameters on Android 13 and below if the target activity is exported. ## Migration guide Depending on your application's use case, you should migrate using one of the following methods: -### For Push Notifications and App Shortcuts -We recommend migrating to use standard Deep Link URIs instead of Intent extras. +### For Push Notifications, App Shortcuts, and Home Screen Widgets +We recommend migrating to use standard [deep links][] instead of `Intent` extras. **Before (Insecure):** ```java @@ -66,15 +67,18 @@ intent.setAction(Intent.ACTION_VIEW); intent.setData(Uri.parse("myapp://product_details")); ``` -Ensure that your `AndroidManifest.xml` declares the corresponding `` for the deep link, as the embedder will query the PackageManager to verify the route is legitimate. +Ensure that your `AndroidManifest.xml` declares the corresponding `` for the deep link, as the embedder will use those `Intent` filters to verify that the route is legitimate. -Alternatively, if you must use Intent extras, configure your push notification to launch a custom `Activity` or `BroadcastReceiver` that you control (and is not exported). Inside that component, construct a new explicit Intent to launch your `FlutterActivity`. Because the new Intent is launched from within your app, the embedder will successfully verify it as self-sent. +Alternatively, if you must use `Intent` extras, you can configure your push notification, shortcut, or widget to launch a custom, unexported `Activity` or `BroadcastReceiver` that you control: + +* **If your target `FlutterActivity` is non-exported:** Your custom component can launch the `FlutterActivity` directly using `startActivity`. Since the target `Activity` is non-exported, it automatically passes the embedder's security verification. +* **If your target `FlutterActivity` must remain exported:** On Android 13 and below, launching it from a `BroadcastReceiver` or `Service` will **fail** verification because `getCallingPackage()` is always `null` for non-Activity context launches. In this scenario, your receiver should launch an intermediate, unexported helper `Activity` first, which then launches the `FlutterActivity` using `startActivityForResult`. ### For Internal Cached Engine or Entrypoint Launches If your app launches custom entrypoints or cached engines internally, you have three primary options: -#### Option 1: Mark host activities as non-exported (Recommended) -If the activity hosting the cached engine or custom entrypoint does not need to be opened by external third-party apps, ensure it is not exported. +#### Option 1: Mark host activities as non-exported (Recommended where possible) +If the `Activity` hosting the cached engine or custom entrypoint does not need to be opened by external third-party apps, ensure it is not exported. In your `AndroidManifest.xml`: ```xml @@ -84,18 +88,20 @@ In your `AndroidManifest.xml`: ``` +> **Note:** The primary launcher `Activity` of your app (the one containing the `android.intent.action.MAIN` and `android.intent.category.LAUNCHER` intent filters) **must** remain exported so that the OS Launcher can start your app. Consequently, you cannot use this option for your main launcher activity. + #### Option 2: Use `startActivityForResult` for internal launches -If the activity must remain exported (e.g. to handle deep links), but you also want to launch it internally with custom entrypoints, routes, or cached engines on legacy Android versions (API 33 and below): +If the activity must remain exported (to handle deep links, for example), but you also want to launch it internally with custom entrypoints, routes, or cached engines on legacy Android versions (API 33 and below): Change your Kotlin/Java caller code to request a result (using a dummy request code): ```diff - startActivity(intent) + startActivityForResult(intent, 0) // 0 is a dummy request code ``` -*Note: You do not need to implement `onActivityResult()` in the caller activity.* +*Note: You do not need to implement [`onActivityResult()`] in the caller activity.* #### Option 3: Configure settings via Manifest `` -For configurations that are static or known at compile time, avoid passing them via Intent extras. Instead, declare them directly in your manifest. +For configurations that are static or known at compile time, avoid passing them via `Intent` extras. Instead, declare them directly in your manifest. In your `AndroidManifest.xml`: ```xml @@ -134,3 +140,36 @@ public class MyFlutterActivity extends FlutterActivity { Landed in version: TBD
In stable release: TBD + +## References + +* [Android `Intent` filters][] +* [Set up Flutter Android deep links][] + +API documentation: + +* [`Radio`][] +* [`CupertinoRadio`][] +* [`RadioListTile`][] +* [`RadioGroup`][] + +Relevant issue: + +* [Issue 190450][] +* [Issue 190452][] + +Relevant PR: + +* [PR 190249][] + +[`Intent`]: https://developer.android.com/reference/android/content/Intent +[`Service`]: https://developer.android.com/reference/android/app/Service +[BroadcastReceiver]: https://developer.android.com/reference/android/content/BroadcastReceiver +[deep links]: https://docs.flutter.dev/cookbook/navigation/set-up-app-links +[intent filters]: https://developer.android.com/guide/components/intents-filters +[onActivityResult()]: https://developer.android.com/reference/android/app/Activity#onActivityResult(int,%20int,%20android.content.Intent) +[Android `Intent` filters]: https://developer.android.com/guide/components/intents-filters +[Set up Flutter Android deep links]: https://docs.flutter.dev/cookbook/navigation/set-up-app-links +[Issue 190450]: https://github.com/flutter/flutter/issues/190450 +[Issue 190452]: https://github.com/flutter/flutter/issues/190452 +[PR 190249]: https://github.com/flutter/flutter/pull/190249 From 3711eb755d6526b7293043fe82f9e60e28ca5121 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Fri, 7 Aug 2026 14:05:59 -0700 Subject: [PATCH 4/6] punctuation --- .../stricter-android-entrypoint-intent-verification.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md b/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md index 6750ee191c..4c2337e44a 100644 --- a/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md +++ b/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md @@ -1,6 +1,6 @@ --- -title: Stricter Android Intent Verification for App Entrypoints -description: Android Intent extras for routing, entrypoints, and cached engines are now strictly verified. +title: Stricter Android `Intent` Verification for App Entrypoints +description: Android `Intent` extras for routing, entrypoints, and cached engines are now strictly verified. --- {% render "docs/breaking-changes.md" %} From 8f80216c9387e0c5e6da8f4d8219920ed6dca19c Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Wed, 12 Aug 2026 12:04:43 -0700 Subject: [PATCH 5/6] self review --- ...-android-entrypoint-intent-verification.md | 184 +++++++++++++----- 1 file changed, 140 insertions(+), 44 deletions(-) diff --git a/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md b/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md index 4c2337e44a..2f80009404 100644 --- a/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md +++ b/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md @@ -1,6 +1,8 @@ --- title: Stricter Android `Intent` Verification for App Entrypoints -description: Android `Intent` extras for routing, entrypoints, and cached engines are now strictly verified. +description: > + Android `Intent` extras for routing, entrypoints, and cached engines + are now strictly verified. --- {% render "docs/breaking-changes.md" %} @@ -8,51 +10,121 @@ description: Android `Intent` extras for routing, entrypoints, and cached engine ## Summary To protect against [`Intent`][]-based vulnerability exploits, -the Flutter Android embedder (`FlutterActivity`, `FlutterFragmentActivity`, and `FlutterFragment`) +the Flutter Android embedder +(`FlutterActivity`, `FlutterFragmentActivity`, and `FlutterFragment`) now strictly verifies the sender of Intents before processing `EXTRA_INITIAL_ROUTE`, `EXTRA_DART_ENTRYPOINT`, `EXTRA_DART_ENTRYPOINT_ARGS`, `EXTRA_CACHED_ENGINE_ID`, and -`EXTRA_CACHED_ENGINE_GROUP_ID`. +`EXTRA_CACHED_ENGINE_GROUP_ID`. External [`Intent`][]s that try to pass these extras (such as `"route"`, `"dart_entrypoint"`, or `"cached_engine_id"`) will now be ignored unless they are verified as originating from the app itself. +:::note +**Unexpected failures or silent drops in Release mode?** +If you see an `Intent verification failed` warning in Debug mode, +or if custom routes suddenly stop working in Release mode without warnings, +one of the following is likely responsible: +- **Third-party Plugins (Push notifications, widgets):** + The plugin author must update their code. + Check `pub.dev` for a plugin update or file an issue. + You do not need to change your own code. +- **Inter-app Communication:** + Another app is trying to launch your app using Intent extras. + You must migrate to using standard Deep Links instead. +- **Automated Testing:** + Your CI/QA scripts are using `adb` + to launch specific routes on a Release APK. +::: + ## Context -Historically, the Flutter Android embedder unconditionally accepted +Before this change, the Flutter Android embedder unconditionally accepted routing, entrypoint, and engine configuration from `Intent` extras. -This made Flutter Android apps vulnerable third-party applications on the same device -to maliciously inject parameters into a Flutter app. +This made Flutter Android apps vulnerable to malicious actors +injecting arbitrary parameters via external Intents. This exposed applications to security vulnerabilities, including: -1. **Route Hijacking:** Forcing the app to load a sensitive internal route (via `EXTRA_INITIAL_ROUTE`). -2. **Entrypoint Injection:** Forcing the app to execute arbitrary Dart functions or passing unsafe parameters (via `EXTRA_DART_ENTRYPOINT` and `EXTRA_DART_ENTRYPOINT_ARGS`). -3. **Session Hijacking/Privilege Escalation:** Reusing an active, pre-authenticated in-memory `FlutterEngine` instance (via `EXTRA_CACHED_ENGINE_ID` and `EXTRA_CACHED_ENGINE_GROUP_ID`) to access user-restricted states. - -To mitigate these issues, we introduced a strict verification check using Android's `getLaunchedFromUid()` (on Android 14+) and `getCallingPackage()` (on Android 13 and below). - -If the sender of the `Intent` cannot be verified as the app itself, the `Intent` extras will be safely ignored. Standard [deep links][] remain supported but are now validated against the app's declared `AndroidManifest.xml` [intent filters][]. +1. **Route Hijacking:** Forcing the app to load a sensitive internal route + (via `EXTRA_INITIAL_ROUTE`). +2. **Entrypoint Injection:** + Forcing the app to execute arbitrary Dart functions + or passing unsafe parameters + (via `EXTRA_DART_ENTRYPOINT` and `EXTRA_DART_ENTRYPOINT_ARGS`). +3. **Session Hijacking/Privilege Escalation:** + Reusing an active, pre-authenticated in-memory `FlutterEngine` instance + (via `EXTRA_CACHED_ENGINE_ID` and `EXTRA_CACHED_ENGINE_GROUP_ID`) + to access user-restricted states. + +To mitigate these issues, we introduced a strict verification check using +Android's `getLaunchedFromUid()` (on Android 14+) +and `getCallingPackage()` (on Android 13 and below). + +If the sender of the `Intent` cannot be verified as the app itself, +the `Intent` extras will be safely ignored. +Standard [deep links][] remain supported +but are now validated against the app's declared `AndroidManifest.xml` +[intent filters][]. ## Description of change -Any **external** system passing the `"route"`, `"dart_entrypoint"`, `"dart_entrypoint_args"`, `"cached_engine_id"`, or `"cached_engine_group_id"` extras will no longer have an effect in release builds. +Any **external** system passing the `"route"`, `"dart_entrypoint"`, +`"dart_entrypoint_args"`, `"cached_engine_id"`, +or `"cached_engine_group_id"` extras +will no longer have an effect in release builds. ### Behavior in Debug/Profile vs. Release -* **Debug and Profile builds:** The verification check is bypassed to preserve developer velocity, CLI testing (`adb shell am start`), and automated instrumentation tests. However, if a verification check would have failed, a warning is logged to alert developers. -* **Release builds:** Enforced strictly. Extras from unverified sources are silently ignored. +* **Debug and Profile builds:** The verification check is bypassed + to preserve developer velocity, CLI testing (`adb shell am start`), + and automated instrumentation tests. + However, if a verification check would have failed, + a warning is logged to alert developers. +* **Release builds:** Enforced strictly. + Extras from unverified sources are silently ignored. ### Affected Scenarios -* **Push Notifications:** If a push notification payload or SDK builds a `PendingIntent` that passes the Flutter `"route"` `Intent` extra (which automatically configures the initial route of the app), this extra will be ignored on Android 13 and below (API 33 and lower) because the OS calling package metadata is lost. On Android 14+ (API 34), this will continue to work. Custom data extras processed manually in Dart are not affected. -* **App Shortcuts and Home Screen Widgets:** Tap actions on widgets or shortcuts that launch `FlutterActivity` directly with route/entrypoint extras will fail verification in release builds because the launch is initiated by the system launcher or system widget host. -* **Internal Cached Engine & Entrypoint Launches:** If an application launches a pre-warmed cached engine or custom entrypoint dynamically from its own native code (e.g. from a background [`Service`][] or [`BroadcastReceiver`][] using standard `startActivity(intent)`), it will fail to resolve the cached engine or entrypoint parameters on Android 13 and below if the target activity is exported. +* **Push Notifications:** If a push notification payload or SDK builds + a `PendingIntent` that passes the Flutter `"route"` `Intent` extra + (which automatically configures the initial route of the app), + this extra will be ignored on Android 13 and below (API 33 and lower) + because the OS calling package metadata is lost. + On Android 14+ (API 34), this will continue to work. + Custom data extras processed manually in Dart are not affected. +* **App Shortcuts and Home Screen Widgets:** Tap actions on widgets + or shortcuts that launch `FlutterActivity` directly + with route/entrypoint extras will fail verification in release builds + because the launch is initiated by the system launcher + or system widget host. +* **Internal Cached Engine & Entrypoint Launches:** + If an application launches a pre-warmed cached engine + or custom entrypoint dynamically from its own native code + (e.g. from a background [`Service`][] or [`BroadcastReceiver`][] + using standard `startActivity(intent)`), + it will fail to resolve the cached engine or entrypoint parameters + on Android 13 and below if the target activity is exported. ## Migration guide -Depending on your application's use case, you should migrate using one of the following methods: +:::note +You are **NOT affected** and do not need to migrate +if **ANY** of the following are true: +- **You do not use custom intent extras:** You only use the default + entrypoint (`main()`) and route (`/`), and do not launch cached engines + (i.e., you never pass `route`, `dart_entrypoint`, `dart_entrypoint_args`, + `cached_engine_id`, or `cached_engine_group_id` as `Intent` extras). +- **Your activity is internal:** Your target `FlutterActivity` or + `FlutterFragmentActivity` is non-exported (`android:exported="false"`). + Since it is non-exported, it automatically passes + the embedder's security verification. +::: + +Depending on your application's use case, +you should migrate using one of the following methods: ### For Push Notifications, App Shortcuts, and Home Screen Widgets -We recommend migrating to use standard [deep links][] instead of `Intent` extras. +We recommend migrating to use standard [deep links][] +instead of `Intent` extras. **Before (Insecure):** ```java @@ -67,41 +139,69 @@ intent.setAction(Intent.ACTION_VIEW); intent.setData(Uri.parse("myapp://product_details")); ``` -Ensure that your `AndroidManifest.xml` declares the corresponding `` for the deep link, as the embedder will use those `Intent` filters to verify that the route is legitimate. - -Alternatively, if you must use `Intent` extras, you can configure your push notification, shortcut, or widget to launch a custom, unexported `Activity` or `BroadcastReceiver` that you control: - -* **If your target `FlutterActivity` is non-exported:** Your custom component can launch the `FlutterActivity` directly using `startActivity`. Since the target `Activity` is non-exported, it automatically passes the embedder's security verification. -* **If your target `FlutterActivity` must remain exported:** On Android 13 and below, launching it from a `BroadcastReceiver` or `Service` will **fail** verification because `getCallingPackage()` is always `null` for non-Activity context launches. In this scenario, your receiver should launch an intermediate, unexported helper `Activity` first, which then launches the `FlutterActivity` using `startActivityForResult`. +Ensure that your `AndroidManifest.xml` declares the corresponding +`` for the deep link, +as the embedder will use those `Intent` filters +to verify that the route is legitimate. + +Alternatively, if you must use `Intent` extras, +you can configure your push notification, shortcut, or widget +to launch a custom, unexported `Activity` or `BroadcastReceiver` +that you control: + +* **If your target `FlutterActivity` is non-exported:** + Your custom component can launch the `FlutterActivity` directly + using `startActivity`. + Since the target `Activity` is non-exported, + it automatically passes the embedder's security verification. +* **If your target `FlutterActivity` must remain exported:** + On Android 13 and below, launching it from a `BroadcastReceiver` or `Service` + will **fail** verification because `getCallingPackage()` is always `null` + for non-Activity context launches. + In this scenario, your receiver should launch an intermediate, + unexported helper `Activity` first, + which then launches the `FlutterActivity` using `startActivityForResult`. ### For Internal Cached Engine or Entrypoint Launches -If your app launches custom entrypoints or cached engines internally, you have three primary options: +If your app launches custom entrypoints or cached engines internally, +you have three primary options: #### Option 1: Mark host activities as non-exported (Recommended where possible) -If the `Activity` hosting the cached engine or custom entrypoint does not need to be opened by external third-party apps, ensure it is not exported. +If the `Activity` hosting the cached engine or custom entrypoint +does not need to be opened by external third-party apps, +ensure it is not exported. In your `AndroidManifest.xml`: ```xml + android:exported="false"> ``` -> **Note:** The primary launcher `Activity` of your app (the one containing the `android.intent.action.MAIN` and `android.intent.category.LAUNCHER` intent filters) **must** remain exported so that the OS Launcher can start your app. Consequently, you cannot use this option for your main launcher activity. +> **Note:** The primary launcher `Activity` of your app +> (the one containing the `android.intent.action.MAIN` +> and `android.intent.category.LAUNCHER` intent filters) +> **must** remain exported so that the OS Launcher can start your app. +> Consequently, you cannot use this option for your main launcher activity. #### Option 2: Use `startActivityForResult` for internal launches -If the activity must remain exported (to handle deep links, for example), but you also want to launch it internally with custom entrypoints, routes, or cached engines on legacy Android versions (API 33 and below): +If the activity must remain exported (to handle deep links, for example), +but you also want to launch it internally with custom entrypoints, routes, +or cached engines on legacy Android versions (API 33 and below): -Change your Kotlin/Java caller code to request a result (using a dummy request code): +Change your Kotlin/Java caller code to request a result +(using a dummy request code): ```diff - startActivity(intent) -+ startActivityForResult(intent, 0) // 0 is a dummy request code ++ startActivityForResult(intent, request_code) ``` *Note: You do not need to implement [`onActivityResult()`] in the caller activity.* #### Option 3: Configure settings via Manifest `` -For configurations that are static or known at compile time, avoid passing them via `Intent` extras. Instead, declare them directly in your manifest. +For configurations that are static or known at compile time, +avoid passing them via `Intent` extras. +Instead, declare them directly in your manifest. In your `AndroidManifest.xml`: ```xml @@ -118,7 +218,10 @@ In your `AndroidManifest.xml`: ``` #### Option 4: Subclass and programmatically define parameters -For dynamic configurations that cannot use `startActivityForResult` (e.g. launches from background `Services` or `BroadcastReceivers`), you can subclass `FlutterActivity` or `FlutterFragmentActivity` and programmatically supply the parameters: +For dynamic configurations that cannot use `startActivityForResult` +(e.g. launches from background `Services` or `BroadcastReceivers`), +you can subclass `FlutterActivity` or `FlutterFragmentActivity` +and programmatically supply the parameters: ```java public class MyFlutterActivity extends FlutterActivity { @@ -146,13 +249,6 @@ In stable release: TBD * [Android `Intent` filters][] * [Set up Flutter Android deep links][] -API documentation: - -* [`Radio`][] -* [`CupertinoRadio`][] -* [`RadioListTile`][] -* [`RadioGroup`][] - Relevant issue: * [Issue 190450][] @@ -164,7 +260,7 @@ Relevant PR: [`Intent`]: https://developer.android.com/reference/android/content/Intent [`Service`]: https://developer.android.com/reference/android/app/Service -[BroadcastReceiver]: https://developer.android.com/reference/android/content/BroadcastReceiver +[`BroadcastReceiver`]: https://developer.android.com/reference/android/content/BroadcastReceiver [deep links]: https://docs.flutter.dev/cookbook/navigation/set-up-app-links [intent filters]: https://developer.android.com/guide/components/intents-filters [onActivityResult()]: https://developer.android.com/reference/android/app/Activity#onActivityResult(int,%20int,%20android.content.Intent) From 162b3c8a84f248ff51f1fe0d514318e945372241 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Wed, 12 Aug 2026 12:22:12 -0700 Subject: [PATCH 6/6] style guide stuff --- ...tricter-android-entrypoint-intent-verification.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md b/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md index 2f80009404..80e2796fea 100644 --- a/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md +++ b/sites/docs/src/content/release/breaking-changes/stricter-android-entrypoint-intent-verification.md @@ -1,6 +1,6 @@ --- title: Stricter Android `Intent` Verification for App Entrypoints -description: > +description: >- Android `Intent` extras for routing, entrypoints, and cached engines are now strictly verified. --- @@ -57,7 +57,7 @@ This exposed applications to security vulnerabilities, including: (via `EXTRA_CACHED_ENGINE_ID` and `EXTRA_CACHED_ENGINE_GROUP_ID`) to access user-restricted states. -To mitigate these issues, we introduced a strict verification check using +To mitigate these issues, Flutter introduced a strict verification check using Android's `getLaunchedFromUid()` (on Android 14+) and `getCallingPackage()` (on Android 13 and below). @@ -99,7 +99,7 @@ will no longer have an effect in release builds. * **Internal Cached Engine & Entrypoint Launches:** If an application launches a pre-warmed cached engine or custom entrypoint dynamically from its own native code - (e.g. from a background [`Service`][] or [`BroadcastReceiver`][] + (for example, from a background [`Service`][] or [`BroadcastReceiver`][] using standard `startActivity(intent)`), it will fail to resolve the cached engine or entrypoint parameters on Android 13 and below if the target activity is exported. @@ -111,7 +111,7 @@ You are **NOT affected** and do not need to migrate if **ANY** of the following are true: - **You do not use custom intent extras:** You only use the default entrypoint (`main()`) and route (`/`), and do not launch cached engines - (i.e., you never pass `route`, `dart_entrypoint`, `dart_entrypoint_args`, + (that is, you never pass `route`, `dart_entrypoint`, `dart_entrypoint_args`, `cached_engine_id`, or `cached_engine_group_id` as `Intent` extras). - **Your activity is internal:** Your target `FlutterActivity` or `FlutterFragmentActivity` is non-exported (`android:exported="false"`). @@ -123,7 +123,7 @@ Depending on your application's use case, you should migrate using one of the following methods: ### For Push Notifications, App Shortcuts, and Home Screen Widgets -We recommend migrating to use standard [deep links][] +Migrate to standard [deep links][] instead of `Intent` extras. **Before (Insecure):** @@ -219,7 +219,7 @@ In your `AndroidManifest.xml`: #### Option 4: Subclass and programmatically define parameters For dynamic configurations that cannot use `startActivityForResult` -(e.g. launches from background `Services` or `BroadcastReceivers`), +(for example, launches from background `Services` or `BroadcastReceivers`), you can subclass `FlutterActivity` or `FlutterFragmentActivity` and programmatically supply the parameters: