From 2e78a1cb0658b5af2537bf667b2b59b7c47c00eb Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Tue, 21 Jul 2026 17:28:54 -0700 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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/9] 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/9] 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: From 78eeb0da761bb8bf6166986c6936d36e5552fade Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 28 Sep 2026 10:17:18 -0700 Subject: [PATCH 7/9] update --- ...-android-entrypoint-intent-verification.md | 318 ++++++++---------- 1 file changed, 134 insertions(+), 184 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 80e2796fea..353581aa2c 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 @@ -9,202 +9,139 @@ description: >- ## Summary -To protect against [`Intent`][]-based vulnerability exploits, +To prevent [`Intent`][]-based vulnerability exploits, the Flutter Android embedder -(`FlutterActivity`, `FlutterFragmentActivity`, and `FlutterFragment`) -now strictly verifies the sender of Intents +(`FlutterActivity` and `FlutterFragmentActivity`) +now verifies the sender of `Intent` objects before processing `EXTRA_INITIAL_ROUTE`, `EXTRA_DART_ENTRYPOINT`, -`EXTRA_DART_ENTRYPOINT_ARGS`, `EXTRA_CACHED_ENGINE_ID`, and -`EXTRA_CACHED_ENGINE_GROUP_ID`. +`EXTRA_DART_ENTRYPOINT_ARGS`, `EXTRA_CACHED_ENGINE_ID`, +and `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. -::: +In release mode, external `Intent` objects cannot set these extras +unless verified as originating from the app itself. +If verification fails, the embedder ignores the extras +and falls back to manifest metadata or default values for initial routes +and Dart entrypoints. ## Context -Before this change, the Flutter Android embedder unconditionally accepted -routing, entrypoint, and engine configuration from `Intent` extras. -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, Flutter 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][]. +Previously, the Flutter Android embedder accepted routing, entrypoint, +and engine configuration directly from `Intent` extras. +This allowed external apps or malicious actors to: + +1. Hijack app navigation to sensitive internal routes + (`EXTRA_INITIAL_ROUTE`). +1. Execute arbitrary Dart entrypoints or pass unsafe arguments + (`EXTRA_DART_ENTRYPOINT` and `EXTRA_DART_ENTRYPOINT_ARGS`). +1. Reuse active, pre-authenticated in-memory engine instances + (`EXTRA_CACHED_ENGINE_ID` and `EXTRA_CACHED_ENGINE_GROUP_ID`). + +To fix these vulnerabilities, Flutter now validates the `Intent` sender. +Deep links remain supported, +but they must match the `` declarations +in your `AndroidManifest.xml`. ## 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 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 - (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. +In release builds, the Android embedder strictly validates `Intent` extras: +`"route"`, `"dart_entrypoint"`, `"dart_entrypoint_args"`, +`"cached_engine_id"`, and `"cached_engine_group_id"`. + +### Intent verification rules + +An `Intent` is verified as safe if: + +* **The activity is not exported:** + Activities with `android:exported="false"` are automatically trusted. +* **On Android 14 and later (API 34+):** + The launching UID matches the application's UID (`getLaunchedFromUid()`). +* **On Android 13 and earlier (API 33 and lower):** + The calling package matches the app package (`getCallingPackage()`). + This requires launching with `startActivityForResult()` + or `startActivityIfNeeded()`. + +For deep links (`Intent.ACTION_VIEW`), +the link URI must match an `` +declared in the application manifest, +unless the `Intent` is verified as self-sent. + +### Fallback behavior + +If verification fails in release builds, the extras are ignored: + +* **Initial route (`EXTRA_INITIAL_ROUTE`):** + Falls back to command-line arguments or `io.flutter.InitialRoute` metadata + in `AndroidManifest.xml`. + If neither is specified, it defaults to `"/"`. +* **Dart entrypoint (`EXTRA_DART_ENTRYPOINT`):** + Falls back to `io.flutter.Entrypoint` metadata in `AndroidManifest.xml`. + If not specified, it defaults to `"main"`. +* **Entrypoint arguments (`EXTRA_DART_ENTRYPOINT_ARGS`):** + Ignored; returns `null`. +* **Cached engine IDs (`EXTRA_CACHED_ENGINE_ID` and + `EXTRA_CACHED_ENGINE_GROUP_ID`):** + Ignored; returns `null`. + +### Behavior in debug and profile modes + +In debug and profile builds, +the embedder bypasses verification to preserve developer tooling +(such as `adb shell am start`) and automated tests. +However, if an `Intent` fails verification, +the embedder logs a warning with the target component, +intent extras, and migration instructions. ## Migration guide -:::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 - (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"`). - Since it is non-exported, it automatically passes - the embedder's security verification. -::: +You do not need to migrate if: -Depending on your application's use case, -you should migrate using one of the following methods: +* You do not pass `route`, `dart_entrypoint`, `dart_entrypoint_args`, + `cached_engine_id`, or `cached_engine_group_id` extras in `Intent`s. +* Your target `FlutterActivity` or `FlutterFragmentActivity` + is not exported (`android:exported="false"`). -### For Push Notifications, App Shortcuts, and Home Screen Widgets -Migrate to standard [deep links][] -instead of `Intent` extras. +If you pass these extras to an exported activity, +choose one of the following migration strategies: -**Before (Insecure):** -```java -Intent intent = new Intent(context, MainActivity.class); -intent.putExtra("route", "/product_details"); -``` +### Set host activities to non-exported -**After (Recommended):** -```java -Intent intent = new Intent(context, MainActivity.class); -intent.setAction(Intent.ACTION_VIEW); -intent.setData(Uri.parse("myapp://product_details")); -``` +If external apps do not need to open your activity directly, +set `android:exported="false"` in `AndroidManifest.xml`: -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: - -#### 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 +```xml title="AndroidManifest.xml" + android:name=".MyFlutterActivity" + 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 +Your app's main launcher activity (with `ACTION_MAIN` +and `CATEGORY_LAUNCHER`) must remain exported so that the system launcher +can launch your app. +::: + +### Migrate to deep links for external routing -#### 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): +To route to specific screens from external sources, +shortcuts, or notifications, +use standard [deep links][] instead of `EXTRA_INITIAL_ROUTE`: -Change your Kotlin/Java caller code to request a result -(using a dummy request code): -```diff -- startActivity(intent) -+ startActivityForResult(intent, request_code) +```java +Intent intent = new Intent(context, MainActivity.class); +intent.setAction(Intent.ACTION_VIEW); +intent.setData(Uri.parse("myapp://product_details")); ``` -*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. +Declare matching `` entries in `AndroidManifest.xml` +so the embedder can validate the incoming URL. + +### Declare entrypoints and routes in AndroidManifest.xml + +For static entrypoints or initial routes, +declare them as `` in `AndroidManifest.xml`: -In your `AndroidManifest.xml`: -```xml +```xml title="AndroidManifest.xml" @@ -217,24 +154,38 @@ In your `AndroidManifest.xml`: ``` -#### Option 4: Subclass and programmatically define parameters -For dynamic configurations that cannot use `startActivityForResult` -(for example, launches from background `Services` or `BroadcastReceivers`), -you can subclass `FlutterActivity` or `FlutterFragmentActivity` -and programmatically supply the parameters: +### Use startActivityForResult on Android 13 and earlier + +For internal app launches to an exported activity on Android 13 and earlier, +call `startActivityForResult()` instead of `startActivity()`: + +```diff +- startActivity(intent); ++ startActivityForResult(intent, REQUEST_CODE); +``` + +This populates `getCallingPackage()`, allowing the embedder to verify +that the `Intent` originated from your app. +You do not need to handle the result in `onActivityResult()`. + +### Override configuration methods in an activity subclass + +To provide dynamic entrypoints or arguments without relying on `Intent` extras, +subclass `FlutterActivity` or `FlutterFragmentActivity` +and override the configuration methods: ```java public class MyFlutterActivity extends FlutterActivity { @NonNull @Override public String getDartEntrypointFunctionName() { - return MyConfigManager.getDynamicEntrypoint(); + return ConfigManager.getEntrypoint(); } @Nullable @Override public List getDartEntrypointArgs() { - return MyConfigManager.getDynamicArgs(); + return ConfigManager.getArgs(); } } ``` @@ -249,7 +200,7 @@ In stable release: TBD * [Android `Intent` filters][] * [Set up Flutter Android deep links][] -Relevant issue: +Relevant issues: * [Issue 190450][] * [Issue 190452][] @@ -259,13 +210,12 @@ 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 +[deep links]: /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 +[Android `Intent` filters]: +https://developer.android.com/guide/components/intents-filters +[Set up Flutter Android deep links]: /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 7716a47ee81bdcd272d95605e1d59e2f3782673b Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 28 Sep 2026 10:19:28 -0700 Subject: [PATCH 8/9] add to index --- sites/docs/src/content/release/breaking-changes/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/docs/src/content/release/breaking-changes/index.md b/sites/docs/src/content/release/breaking-changes/index.md index 116491f607..1f4c0f6f1a 100644 --- a/sites/docs/src/content/release/breaking-changes/index.md +++ b/sites/docs/src/content/release/breaking-changes/index.md @@ -38,10 +38,12 @@ They're sorted by release and listed in alphabetical order: * [Added enabled property and made onChanged optional for DropdownButton][] * [Migrate to standalone `material_ui` and `cupertino_ui` packages][] +* [Stricter Android `Intent` Verification for App Entrypoints][] * [Restrict command-line flags for prebuilt Android release binaries][] * [Removal of `useInheritedMediaQuery`][] [Added enabled property and made onChanged optional for DropdownButton]: /release/breaking-changes/dropdownbutton-enabled-property +[Stricter Android `Intent` Verification for App Entrypoints]: /release/breaking-changes/stricter-android-entrypoint-intent-verification [Migrate to standalone `material_ui` and `cupertino_ui` packages]: /release/breaking-changes/material-ui-and-cupertino-ui [Restrict command-line flags for prebuilt Android release binaries]: /release/breaking-changes/restrict-command-line-flags-prebuilt-android-release-binaries [Removal of `useInheritedMediaQuery`]: /release/breaking-changes/remove-useInheritedMediaQuery From 88086fb74177671dd212db524de581568252c62f Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 5 Oct 2026 11:11:00 -0700 Subject: [PATCH 9/9] fix capitalization --- sites/docs/src/content/release/breaking-changes/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sites/docs/src/content/release/breaking-changes/index.md b/sites/docs/src/content/release/breaking-changes/index.md index 1f4c0f6f1a..3129a40435 100644 --- a/sites/docs/src/content/release/breaking-changes/index.md +++ b/sites/docs/src/content/release/breaking-changes/index.md @@ -38,12 +38,12 @@ They're sorted by release and listed in alphabetical order: * [Added enabled property and made onChanged optional for DropdownButton][] * [Migrate to standalone `material_ui` and `cupertino_ui` packages][] -* [Stricter Android `Intent` Verification for App Entrypoints][] +* [Stricter Android `Intent` verification for app entrypoints][] * [Restrict command-line flags for prebuilt Android release binaries][] * [Removal of `useInheritedMediaQuery`][] [Added enabled property and made onChanged optional for DropdownButton]: /release/breaking-changes/dropdownbutton-enabled-property -[Stricter Android `Intent` Verification for App Entrypoints]: /release/breaking-changes/stricter-android-entrypoint-intent-verification +[Stricter Android `Intent` verification for app entrypoints]: /release/breaking-changes/stricter-android-entrypoint-intent-verification [Migrate to standalone `material_ui` and `cupertino_ui` packages]: /release/breaking-changes/material-ui-and-cupertino-ui [Restrict command-line flags for prebuilt Android release binaries]: /release/breaking-changes/restrict-command-line-flags-prebuilt-android-release-binaries [Removal of `useInheritedMediaQuery`]: /release/breaking-changes/remove-useInheritedMediaQuery