Skip to content
2 changes: 2 additions & 0 deletions sites/docs/src/content/release/breaking-changes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 Android engine flags in release mode][]
* [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 Android engine flags in release mode]: /release/breaking-changes/restrict-android-engine-flags-release-mode
[Removal of `useInheritedMediaQuery`]: /release/breaking-changes/remove-useInheritedMediaQuery
Comment on lines 39 to 49

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The breaking changes list and reference links should be sorted in alphabetical order, as noted in the section header ("They're sorted by release and listed in alphabetical order:"). Currently, "Stricter Android Intent Verification..." is placed out of order.

Please reorder the list items and reference links as follows:

* [Added enabled property and made onChanged optional for DropdownButton][]
* [Migrate to standalone `material_ui` and `cupertino_ui` packages][]
* [Removal of `useInheritedMediaQuery`][]
* [Restrict command-line flags for prebuilt Android release binaries][]
* [Stricter Android `Intent` Verification for App Entrypoints][]

[Added enabled property and made onChanged optional for DropdownButton]: /release/breaking-changes/dropdownbutton-enabled-property
[Migrate to standalone `material_ui` and `cupertino_ui` packages]: /release/breaking-changes/material-ui-and-cupertino-ui
[Removal of `useInheritedMediaQuery`]: /release/breaking-changes/remove-useInheritedMediaQuery
[Restrict command-line flags for prebuilt Android release binaries]: /release/breaking-changes/restrict-command-line-flags-prebuilt-android-release-binaries
[Stricter Android `Intent` Verification for App Entrypoints]: /release/breaking-changes/stricter-android-entrypoint-intent-verification
References
  1. The repository style guide emphasizes respecting the repository structure and following established conventions. The breaking changes index explicitly states that entries are sorted in alphabetical order. (link)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessarily true but I can fix if so!

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
---
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 prevent [`Intent`][]-based vulnerability exploits,
the Flutter Android embedder
(`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`.

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

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 `<intent-filter>` declarations
in your `AndroidManifest.xml`.

## Description of change

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 `<intent-filter>`
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

You do not need to migrate if:

* 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"`).

If you pass these extras to an exported activity,
choose one of the following migration strategies:

### Set host activities to non-exported

If external apps do not need to open your activity directly,
set `android:exported="false"` in `AndroidManifest.xml`:

```xml title="AndroidManifest.xml"
<activity
android:name=".MyFlutterActivity"
android:exported="false">
</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

To route to specific screens from external sources,
shortcuts, or notifications,
use standard [deep links][] instead of `EXTRA_INITIAL_ROUTE`:

```java
Intent intent = new Intent(context, MainActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse("myapp://product_details"));
```

Declare matching `<intent-filter>` 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 `<meta-data>` in `AndroidManifest.xml`:

```xml title="AndroidManifest.xml"
<activity
android:name=".MyFlutterActivity"
android:exported="true">
<meta-data
android:name="io.flutter.Entrypoint"
android:value="myCustomEntrypoint" />
<meta-data
android:name="io.flutter.InitialRoute"
android:value="/customRoute" />
</activity>
```

### 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 ConfigManager.getEntrypoint();
}

@Nullable
@Override
public List<String> getDartEntrypointArgs() {
return ConfigManager.getArgs();
}
}
```

## Timeline

Landed in version: TBD<br>
In stable release: TBD

## References

* [Android `Intent` filters][]
* [Set up Flutter Android deep links][]

Relevant issues:

* [Issue 190450][]
* [Issue 190452][]

Relevant PR:

* [PR 190249][]

[`Intent`]: https://developer.android.com/reference/android/content/Intent
[deep links]: /cookbook/navigation/set-up-app-links
[intent filters]: https://developer.android.com/guide/components/intents-filters
[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

Loading