State-changing action invoked with an auto-execute / bypass-confirmation flag

ID

dart.platform.deeplink_autoexecute

Severity

critical

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Platform

Language

Dart

Description

A state-changing navigation or constructor is invoked with an auto-execute / auto-confirm / bypass-confirmation flag set (autoExecute: true, autoSubmit: true, autoConfirm: true, bypassConfirmation: true, requireConfirmation: false, etc.). When the call site is a deeplink handler, ANY app or website that can launch the URI can trigger this action against the current user.

Rationale

The classic Flutter deeplink-CSRF pattern: an app registers a custom URI scheme (e.g. myapp://transfer?to=…&amount=…), and the deeplink handler pushes a screen with the parameters AND an autoExecute: true flag that skips the confirmation step. A malicious app or website calls Intent.parseUri("myapp://transfer?to=Attacker&amount=99999"), the OS hands the link to your app, your handler fires the pre-confirmed transfer, and the money moves.

The same anti-pattern shows up for:

  • One-tap account deletion / data wipe.

  • One-tap purchase / subscription activation.

  • One-tap "trust this device" / second-factor enrollment.

  • One-tap administrative actions in dev / admin flows.

Auto-execute flags are dangerous regardless of the call site, but the deeplink scenario makes them attacker-reachable from outside the app. The rule fires on the flag itself rather than on a path- from-deeplink-to-flag (which would need taint mode) — accept some false positives in exchange for catching the pattern at all.

The following code illustrates a vulnerable pattern detected by this rule:

class TransferScreen extends StatelessWidget {
  const TransferScreen({super.key, this.recipient, this.amount, this.autoExecute = false});
  final String? recipient;
  final String? amount;
  final bool autoExecute;
  @override
  Widget build(BuildContext context) => const Placeholder();
}

class DeeplinkHandlers {
  void handleTransfer(BuildContext context, Uri uri) {
    final String? to = uri.queryParameters['to'];
    final String? amount = uri.queryParameters['amount'];

    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => TransferScreen(
          recipient: to,
          amount: amount,
          // VULNERABLE: State-changing action invoked with an auto-execute / bypass-confirmation flag
          autoExecute: true,
        ),
      ),
    );
  }
}

Remediation

Deeplinks must only navigate or pre-fill. State-changing actions require an explicit user confirmation gesture and, for high-value operations, re-authentication via local_auth (with biometricOnly: true for biometric-only).

// Vulnerable
Navigator.push(context, MaterialPageRoute(
  builder: (_) => TransferScreen(
    recipient: to,
    amount: amount,
    autoExecute: true,         // any caller of the deeplink wins
  ),
));

// Fixed
Navigator.push(context, MaterialPageRoute(
  builder: (_) => TransferScreen(
    recipient: to,
    amount: amount,
    autoExecute: false,        // user must press Confirm
    requireBiometric: true,    // and re-authenticate
  ),
));

Configuration

This detector does not need any configuration.

References

© 2026 Xygeni. All rights reserved