fix race condition in deep link loading (#2709)
What changed, and why it matters
This commit fixes a race condition in how the app handles deep links (special URLs that open the app from outside, like payment requests or WalletConnect connections). Before the fix, the app might try to act on a deep link before the wallet dashboard was fully loaded, which could cause the link to be ignored or processed in the wrong state. The fix adds a flag that delays deep-link handling until after the main wallet page is ready. There is no direct evidence in the commit that this was exploitable as a security vulnerability, but race conditions in deep-link handling can sometimes be abused to redirect users or trigger unintended actions.
Treat as a reliability/robustness fix. If deep links are security-sensitive (payment URIs, WalletConnect sessions), review whether any cached deep link could still be processed after an unexpected state transition, and confirm that `currentLink` is cleared appropriately after handling. No immediate security patch is indicated by the diff alone.
Security signals we found
Race condition in deep-link/deferred-link handling
Timing gap between authentication state change and page navigation
Potential for deep link to be dropped or acted on in wrong UI/wallet state
No input validation changes or cryptographic fixes present
Evidence from the diff
The patch modifies on_authentication_state_change.dart and link_view_model.dart. It introduces a readyToOpenPage boolean in LinkViewModel and a markReadyToOpenPage() method that sets this flag and then calls handleLink(). The authentication-state reaction now calls markReadyToOpenPage() inside a WidgetsBinding.instance.addPostFrameCallback after navigating to the dashboard, ensuring the widget tree is built before deep-link processing. handleLink() now early-returns if readyToOpenPage is false. This prevents deep links received before the dashboard is ready from being processed prematurely.
Changed components
lib/reactions/on_authentication_state_change.dartlib/view_model/link_view_model.dartDeep link / URI handling subsystemWalletConnect and payment URI routingInspect captured patch +23 / −11
diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart
index cdbe39a3..d7652f78 100644
--- a/lib/reactions/on_authentication_state_change.dart
+++ b/lib/reactions/on_authentication_state_change.dart
@@ -15,6 +15,7 @@ import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
+import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/widgets.dart';
@@ -40,23 +41,19 @@ void startAuthenticationStateChange(
_onAuthenticationStateChange ??= autorun((_) async {
final state = authenticationStore.state;
- if (state == AuthenticationState.installed &&
- !SettingsStoreBase.walletPasswordDirectInput) {
+ if (state == AuthenticationState.installed && !SettingsStoreBase.walletPasswordDirectInput) {
try {
if (!(await requireHardwareWalletConnection())) await loadCurrentWallet();
} catch (error, stack) {
loginError = error;
await ExceptionHandler.resetLastPopupDate();
- await ExceptionHandler.onError(
- FlutterErrorDetails(exception: error, stack: stack));
+ await ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack));
}
return;
}
- if ([AuthenticationState.allowed, AuthenticationState.allowedCreate]
- .contains(state)) {
- if (state == AuthenticationState.allowed &&
- (await requireHardwareWalletConnection())) {
+ if ([AuthenticationState.allowed, AuthenticationState.allowedCreate].contains(state)) {
+ if (state == AuthenticationState.allowed && (await requireHardwareWalletConnection())) {
await navigatorKey.currentState!.pushNamedAndRemoveUntil(
Routes.connectDevices,
(route) => false,
@@ -116,15 +113,18 @@ void startAuthenticationStateChange(
// await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.connectDevices, (route) => false, arguments: ConnectDevicePageParams(walletType: walletType, onConnectDevice: onConnectDevice));
} else {
- await navigatorKey.currentState!
- .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
+ navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
}
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ final linkViewModel = getIt.get<LinkViewModel>();
+ linkViewModel.markReadyToOpenPage();
+ });
+
if (!(await authenticatedErrorStreamController.stream.isEmpty)) {
await ExceptionHandler.showError(
(await authenticatedErrorStreamController.stream.first).toString());
authenticatedErrorStreamController.stream.drain();
}
- return;
}
});
}
diff --git a/lib/view_model/link_view_model.dart b/lib/view_model/link_view_model.dart
index fdffb982..513f8d58 100644
--- a/lib/view_model/link_view_model.dart
+++ b/lib/view_model/link_view_model.dart
@@ -1,3 +1,5 @@
+import 'dart:async';
+
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/routes.dart';
@@ -21,6 +23,7 @@ class LinkViewModel {
final AuthenticationStore authenticationStore;
final GlobalKey<NavigatorState> navigatorKey;
Uri? currentLink;
+ bool readyToOpenPage = false;
bool get _isValidPaymentUri => currentLink?.path.isNotEmpty ?? false;
bool get isWalletConnectLink => currentLink?.authority == 'wc';
@@ -93,7 +96,16 @@ class LinkViewModel {
} catch (_) {}
}
+ Future<void> markReadyToOpenPage() async {
+ readyToOpenPage = true;
+ await handleLink();
+ }
+
Future<void> handleLink() async {
+ if (!readyToOpenPage) {
+ return;
+ }
+
String? route = getRouteToGo();
dynamic args = getRouteArgs();
if (route != null) {
Why this scored 45/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.