Skip to content

· 10 min read · Nadeera Silva

Mobile

Flutter 3.50: Stricter Android Intent Verification

Flutter 3.50 ignores route and entrypoint Intent extras from other apps in release builds. Here is who is affected and how to migrate.

A teal robot gatekeeper at the arched doorway of a phone-shaped building lets blank teal envelopes in from a conveyor while a barrier arm stops an amber envelope delivered by an outside drone, standing for Flutter 3.50 checking who sent an Android Intent before using its extras.

Flutter 3.50 changes how your Android app treats the Intent that opens it. In the 3.50 beta, FlutterActivity and FlutterFragmentActivity stop trusting the route, Dart entrypoint and cached engine extras on an Intent unless your own app sent it. If your app opens Flutter screens from notifications, shortcuts, home screen widgets, plugins or adb scripts, some of those launches will quietly land on / in release builds. This post covers what changed, why, how to check whether you are affected, and the manifest and Kotlin changes that keep each launch path working.

What changed in Flutter 3.50

The breaking-change page says the Android embedder now verifies the sender before it reads five Intent extras: route (EXTRA_INITIAL_ROUTE), dart_entrypoint, dart_entrypoint_args, cached_engine_id and cached_engine_group_id. In release mode, an Intent that can't be verified as coming from your app has those extras ignored.

The change landed in 3.50.0-0.1.pre on 14 October 2026. The current beta is 3.50.0-0.2.pre (24 October, Colombo time), which also ships Dart 3.14.0-315.3.beta. Stable is still 3.47.8. The release schedule targets Flutter 3.50 stable for November 2026 but gives no exact day. The code was merged on 26 September, before the 6 October branch cutoff, so I expect it to ship in 3.50 stable unchanged. Both breaking-change pages still say "In stable release: Not yet".

Why the Flutter team did this

Two Android security bugs explain it. In issue #190450, the default MainActivity is exported (it has to be, for the launcher), and the embedder parsed the data URI of any explicit Intent without checking the action, the caller or your intent filters. A co-resident app with zero permissions could send an explicit Intent straight to MainActivity, skip Android App Links domain verification, and push your app to a sensitive route.

Issue #190452 is worse. Any app could pass dart_entrypoint and dart_entrypoint_args to make your app boot a different @pragma('vm:entry-point') function, with any arguments, inside your app's own sandbox. The docs add a third risk: reusing a cached, already signed-in FlutterEngine through the cached engine extras.

Both issues were fixed by PR #190249. I couldn't find a CVE or GitHub security advisory for either one. They are tracked only as internal Google bugs.

The Flutter 3.50 Android Intent verification rules

Flowchart of how Flutter 3.50 handles an Intent at FlutterActivity: debug and profile builds bypass the check, release builds trust the Intent only if the activity is not exported, the launching UID matches on Android 14 and later, or the calling package matches on Android 13 and earlier, otherwise extras fall back to manifest meta-data and a data URI is used only for a matching ACTION_VIEW deep link
How the 3.50 embedder decides whether to trust an Intent

The logic lives in a new class, IntentUtils.java. I read it on main to confirm the order of the checks. An Intent counts as self-sent if any of these is true:

SituationRule in release buildsWhat you need to do
Activity has android:exported="false"Always trustedNothing
Android 14 and later (API 34+)Trusted if getLaunchedFromUid() equals your app's UIDLaunch from your own code
Android 13 and earlier (API 33 and lower)Trusted if getCallingPackage() equals your packageLaunch with startActivityForResult() or startActivityIfNeeded()
Debug or profile buildCheck bypassed, warning loggedRead the warnings before you ship

Deep links follow a separate path. If the Intent isn't self-sent, the embedder still uses its data URI as the route, but only when the action is ACTION_VIEW and the URI matches an <intent-filter> that resolves to that activity. An explicit Intent with a URI but no ACTION_VIEW gets ignored, which closes the App Links bypass.

What falls back to what

When verification fails in release, the docs list these fallbacks:

  • Route: the command-line route or io.flutter.InitialRoute meta-data, otherwise "/".
  • Dart entrypoint: io.flutter.Entrypoint meta-data, otherwise "main".
  • Entrypoint arguments: null.
  • Cached engine ID and group ID: null, so a fresh engine starts instead of reusing a cached one.

Who is affected

You don't need to change anything if you never pass those five extras, or if the Flutter activity that receives them is not exported. Go through this list if you're not sure:

  • Notifications or shortcuts built in native code that put route into a PendingIntent aimed at an exported activity.
  • Home screen widgets or add-to-app hosts that call FlutterActivity.withNewEngine().initialRoute(...) or withCachedEngine(...) and then use plain startActivity(). On Android 13 and earlier, those launches now fail the check.
  • A second exported Flutter activity that uses dart_entrypoint to run a different main.
  • QA or CI scripts that run adb shell am start ... --es route /something against a release APK.
  • Explicit Intents that carry a deep link URI without ACTION_VIEW, or URIs your manifest doesn't declare.
  • Release-build CI that passes engine flags (more on that below).

The PR author checked several popular plugins and reported they won't break: all first-party plugins, firebase_messaging 16.5.0, flutter_local_notifications 22.3.0, awesome_notifications 0.12.1, android_alarm_manager_plus 5.1.1, receive_sharing_intent 1.9.0, onesignal_flutter, workmanager 0.10.7, flutter_background_service 5.1.0, app_links 7.2.1 and uni_links 0.5.1. Older versions of these weren't part of that check. The same PR notes that on Android 13 and earlier the sender can't always be verified, so unmigrated plugins can still break on those devices.

go_router has published nothing specific about 3.50. It gets deep links from the engine, so it follows the deep link rule above: a correct ACTION_VIEW link that matches your filter still routes.

Migrating your app

These follow the migration options in the official docs. I've written them in Kotlin, since new Flutter projects use Kotlin for the Android side.

1. Make internal Flutter activities non-exported

If only your own app opens an activity, take it off the exported list. Your launcher MainActivity has to stay exported.

<activity
    android:name=".SettingsFlutterActivity"
    android:exported="false"
    android:theme="@style/LaunchTheme" />

2. Use real deep links (ACTION_VIEW and an intent filter) for notifications and shortcuts

Replace route extras with an ACTION_VIEW deep link and a matching filter. Flutter's built-in deep link handling has to be on. It defaults to on, but check that you haven't set flutter_deeplinking_enabled to false.

<activity android:name=".MainActivity" android:exported="true">
    <!-- launcher intent-filter stays as it is -->
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="myapp" android:host="orders" />
    </intent-filter>
</activity>
val intent = Intent(
    Intent.ACTION_VIEW,
    Uri.parse("myapp://orders/42"),
    context,
    MainActivity::class.java,
)
val pendingIntent = PendingIntent.getActivity(
    context,
    42,
    intent,
    PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)

3. Declare static entrypoints and routes in the manifest

If an activity always opens the same entrypoint or route, put that in meta-data, so it no longer depends on the Intent.

<activity android:name=".WidgetFlutterActivity" android:exported="true">
    <meta-data
        android:name="io.flutter.Entrypoint"
        android:value="widgetMain" />
    <meta-data
        android:name="io.flutter.InitialRoute"
        android:value="/widget" />
</activity>

4. Use startActivityForResult for internal launches on Android 13 and earlier

On API 33 and lower, getCallingPackage() is only set when the caller expects a result. You don't need to handle the result.

private const val FLUTTER_LAUNCH = 1001

fun openFlutterSettings(activity: Activity) {
    val intent = FlutterActivity
        .withNewEngine()
        .initialRoute("/settings")
        .build(activity)
    // Populates getCallingPackage() on API 33 and lower.
    activity.startActivityForResult(intent, FLUTTER_LAUNCH)
}

startActivityForResult() is deprecated on ComponentActivity in favour of the Activity Result APIs, but it's what the Flutter docs recommend for this case, and it still works.

5. Subclass FlutterActivity for dynamic entrypoints

If the entrypoint or arguments depend on runtime state, override the configuration methods instead of using extras:

class WidgetFlutterActivity : FlutterActivity() {
    override fun getDartEntrypointFunctionName(): String =
        "widgetMain"

    override fun getDartEntrypointArgs(): List<String>? =
        listOf(WidgetConfig.currentWidgetId(this))
}

Engine flags are now ignored in release builds

A related change, Restrict Android engine flags in release mode, also landed in 3.50.0-0.1.pre. Release builds now ignore engine flags such as --dart-flags, --route and tracing flags when they come in through Intent extras, and log a warning. Release builds read configuration only from the compiled manifest or from code that runs before the engine starts. Debug and profile builds still accept these flags. The plan is in issue #190461.

The CLI change matters most for CI. flutter run --release --use-application-binary with engine flags is now a fatal error, because a prebuilt APK's manifest can't be changed. Normal flutter build apk --release and flutter build appbundle work as before, since the CLI writes the flags into the manifest at build time.

If you need flags in a release binary, declare them under <application>:

<application android:label="my_app" android:name="${applicationName}">
    <meta-data
        android:name="io.flutter.embedding.android.DartFlags"
        android:value="--some-dart-flag" />
</application>

For benchmarks and integration tests that change flags at launch, switch to --profile. In native code, override getFlutterEngineFlags() in your FlutterActivity subclass (getFlutterShellArgs is deprecated), use FlutterFragment.withNewEngine().flutterEngineFlags(...), or pass arguments to the FlutterEngine(context, args) constructor.

How to test on the 3.50 beta

I haven't run a device test for this post. These are the commands I'd use, based on the docs and the source.

# Option A: switch channel
flutter channel beta && flutter upgrade

# Option B: FVM, pinned to the current beta
fvm install 3.50.0-0.2.pre
fvm use 3.50.0-0.2.pre

flutter build apk --release
adb install -r build/app/outputs/flutter-apk/app-release.apk

adb shell runs as the shell user, not as your app, so these launches count as external:

# Route extra on an explicit Intent: expected to open "/" in release
adb shell am start -n com.example.app/.MainActivity --es route /admin

# URI without ACTION_VIEW: expected to be ignored in release
adb shell am start -n com.example.app/.MainActivity -d "myapp://orders/42"

# Proper deep link that matches your filter: should still open the order
adb shell am start -a android.intent.action.VIEW -d "myapp://orders/42" com.example.app

Then install a debug build and run the same commands while watching adb logcat | grep IntentUtils. Each launch your app should be making internally shows up as a warning to fix. Test on one Android 13 device and one Android 14 or later device, because the rule differs between them.

The fix doesn't cover everything yet. A few extras, such as background mode and state restoration, are still read directly, and issue #192076 tracks moving them to the safe accessors.

Dart 3.14 in the same release

Flutter 3.50 beta bundles Dart 3.14. The Dart SDK changelog still marks 3.14.0 as "Unreleased", so the details could change. The parts that matter most for app teams:

  • A new dart migrate command raises a package's SDK lower bound one minor version at a time and applies the matching fixes. Use --dry-run to preview and --apply to write. --target-sdk and --step (prepare, bump, cleanup, all) control how far it goes. It needs packages already on Dart 3.0 or later, and it isn't the old null-safety tool.
  • dart:mirrors is now deprecated.
  • dart pub upgrade accepts package@latest, package@constraint and package@resolvable.
  • There's a new future_sync_value lint, and dart run -e evaluates inline code.
  • Breaking: the sync isolate APIs accidentally exposed in 3.13 (Isolate.runSync and related) are removed, and SIMD toString() now returns 'V128'.

If you maintain an older app and plan to jump several versions at once, the checklist in taking over a live Flutter app is a good place to start. Test the beta first and leave the move to stable until 3.50 is actually released.

Frequently asked questions

What changed with Android Intents in Flutter 3.50?

FlutterActivity and FlutterFragmentActivity now check who sent an Intent before using the route, dart_entrypoint, dart_entrypoint_args, cached_engine_id and cached_engine_group_id extras. In release builds those extras are ignored unless the Intent came from your own app or the activity is not exported.

Does Flutter 3.50 block explicit Intents from other Android apps?

No, other apps can still start your exported Flutter activity with an explicit Intent. In release builds Flutter 3.50 just ignores the route, entrypoint and cached engine extras on it unless the sender is your own app, and a URI is only routed when it arrives as ACTION_VIEW and matches an intent filter in your manifest.

Why does my Flutter notification open the home screen instead of the right page on 3.50?

The notification probably passes a route extra to an exported activity, and in release builds Flutter 3.50 ignores that extra when it cannot verify the sender. Switch the notification to an ACTION_VIEW deep link that matches an intent filter in your manifest.

Do Flutter deep links still work in 3.50?

Yes. A deep link still opens its route if the Intent uses ACTION_VIEW and the URI matches an intent filter declared for that activity. Explicit Intents that carry a URI without ACTION_VIEW, or with a URI the manifest does not declare, are ignored in release builds.

Why does adb shell am start with a route extra no longer work on my release build?

adb runs as the shell user, so Flutter 3.50 treats the launch as external and ignores the route extra in release mode. Debug and profile builds still accept it and log a warning, so use a profile build for scripted tests.

Is there a CVE for the Flutter Android Intent vulnerability?

No CVE or GitHub security advisory had been published as of 27 October 2026. The issues are tracked as flutter/flutter issues 190450 and 190452 and internal Google bugs, and were fixed by pull request 190249.

When is Flutter 3.50 stable released?

The Flutter release schedule targets November 2026 for 3.50 stable without an exact day. As of 27 October 2026 the latest beta is 3.50.0-0.2.pre and the latest stable is 3.47.8.

Sources

  1. Stricter Android Intent verification for app entrypoints (Flutter docs, 2026-10-14)
  2. Restrict Android engine flags in release mode (Flutter docs, 2026-10-14)
  3. Breaking changes and migration guides (Flutter docs, 2026-10-26)
  4. Flutter SDK releases feed (Linux) (Flutter)
  5. Flutter SDK archive and 2026 release schedule (Flutter docs)
  6. [Android] Refuse external setters of engine entrypoint and cached engine arguments via Intents (PR #190249) (flutter/flutter on GitHub, 2026-09-26)
  7. [Android] Arbitrary route injection via unvalidated explicit Intents (#190450) (flutter/flutter on GitHub, 2026-08-03)
  8. [Android] Unauthorized executing of arbitrary Dart entrypoints via Intent extras (#190452) (flutter/flutter on GitHub, 2026-08-03)
  9. [Android] Restrict engine flags via Intent extras to non-release modes (#190461) (flutter/flutter on GitHub, 2026-08-03)
  10. [Android] Migrate remaining internal Intent.get*Extra calls to IntentUtils (#192076) (flutter/flutter on GitHub, 2026-08-31)
  11. IntentUtils.java (Flutter Android embedder source) (flutter/flutter on GitHub)
  12. Dart SDK CHANGELOG (3.14.0, unreleased) (Dart)
  13. Deep linking (Flutter docs)
SharePost on XLinkedIn

Comments

Share a question or note about this article.

Loading comments…