CVE-2026-11131
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
OtpVerificationDialogBridgechrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.java |
modified |
Files Changed
chrome/browser/ui/android/autofill/internal/BUILD.gnchrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.javachrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridgeTest.java
Patch
From 6ce3ab538b82d1a1c6f78fc589d5f94f660bb1a9 Mon Sep 17 00:00:00 2001 From: Matthias Koerber <[email protected]> Date: Mon, 13 Apr 2026 05:47:07 -0700 Subject: [PATCH] Fix issue in OTP dialog The native OtpVerificationDialogViewAndroid C++ object is destroyed synchronously upon dismissal, but the Android UI remains interactive during a 200ms fade-out animation. Interactions during this window could invoke JNI methods using a stale pointer. This CL fixes the issue by: 1. Clearing the native pointer in OtpVerificationDialogBridge when dismissal is notified. 2. Adding null checks before calling native methods in the bridge. Bug: 501561644 Change-Id: I31e2607416056eae23aad6603409cf65e459886b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7754581 Reviewed-by: Piotr Kotynia <[email protected]> Commit-Queue: Piotr Kotynia <[email protected]> Auto-Submit: Matthias Körber <[email protected]> Cr-Commit-Position: refs/heads/main@{#1613646} --- diff --git a/chrome/browser/ui/android/autofill/internal/BUILD.gn b/chrome/browser/ui/android/autofill/internal/BUILD.gn index d826e92c..8e09362 100644 --- a/chrome/browser/ui/android/autofill/internal/BUILD.gn +++ b/chrome/browser/ui/android/autofill/internal/BUILD.gn @@ -68,6 +68,7 @@ "java/src/org/chromium/chrome/browser/ui/autofill/AuthenticatorSelectionDialogTest.java", "java/src/org/chromium/chrome/browser/ui/autofill/AutofillErrorDialogBridgeTest.java", "java/src/org/chromium/chrome/browser/ui/autofill/AutofillProgressDialogBridgeTest.java", + "java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridgeTest.java", "java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogTest.java", "java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowBridgeTest.java", "java/src/org/chromium/chrome/browser/ui/autofill/ephemeraltab/PaymentsWindowCoordinatorTest.java", diff --git a/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.java b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.java index 7b0d098f..724d741 100644 --- a/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.java +++ b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.java @@ -22,7 +22,7 @@ @JNINamespace("autofill") @NullMarked class OtpVerificationDialogBridge implements OtpVerificationDialogCoordinator.Delegate { - private final long mNativeOtpVerificationDialogView; + private long mNativeOtpVerificationDialogView; private final OtpVerificationDialogCoordinator mDialogCoordinator; OtpVerificationDialogBridge( @@ -55,17 +55,21 @@ @Override public void onConfirm(String otp) { + if (mNativeOtpVerificationDialogView == 0) return; OtpVerificationDialogBridgeJni.get().onConfirm(mNativeOtpVerificationDialogView, otp); } @Override public void onNewOtpRequested() { + if (mNativeOtpVerificationDialogView == 0) return; OtpVerificationDialogBridgeJni.get().onNewOtpRequested(mNativeOtpVerificationDialogView); } @Override public void onDialogDismissed() { + if (mNativeOtpVerificationDialogView == 0) return; OtpVerificationDialogBridgeJni.get().onDialogDismissed(mNativeOtpVerificationDialogView); + mNativeOtpVerificationDialogView = 0; } /** diff --git a/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridgeTest.java b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridgeTest.java new file mode 100644 index 0000000..2f17326 --- /dev/null +++ b/chrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridgeTest.java @@ -0,0 +1,95 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.ui.autofill; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import org.chromium.base.test.BaseRobolectricTestRunner; +import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogType; +import org.chromium.ui.test.util.modaldialog.FakeModalDialogManager; + +/** Unit tests for {@link OtpVerificationDialogBridge}. */ +@RunWith(BaseRobolectricTestRunner.class) +public class OtpVerificationDialogBridgeTest { + private static final long NATIVE_OTP_VERIFICATION_DIALOG_VIEW = 100L; + + private FakeModalDialogManager mModalDialogManager; + private OtpVerificationDialogBridge mOtpVerificationDialogBridge; + @Mock private OtpVerificationDialogBridge.Natives mNativeMock; + @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Before + public void setUp() { + mModalDialogManager = new FakeModalDialogManager(ModalDialogType.TAB); + mOtpVerificationDialogBridge = + new OtpVerificationDialogBridge( + NATIVE_OTP_VERIFICATION_DIALOG_VIEW, + ApplicationProvider.getApplicationContext(), + mModalDialogManager); + OtpVerificationDialogBridgeJni.setInstanceForTesting(mNativeMock); + } + + @Test + @SmallTest + public void testOnConfirm_callsNative() { + mOtpVerificationDialogBridge.onConfirm("123456"); + + verify(mNativeMock, times(1)).onConfirm(NATIVE_OTP_VERIFICATION_DIALOG_VIEW, "123456"); + } + + @Test + @SmallTest + public void testOnNewOtpRequested_callsNative() { + mOtpVerificationDialogBridge.onNewOtpRequested(); + + verify(mNativeMock, times(1)).onNewOtpRequested(NATIVE_OTP_VERIFICATION_DIALOG_VIEW); + } + + @Test + @SmallTest + public void testOnDialogDismissed_callsNativeAndClearsPointer() { + mOtpVerificationDialogBridge.onDialogDismissed(); + + verify(mNativeMock, times(1)).onDialogDismissed(NATIVE_OTP_VERIFICATION_DIALOG_VIEW); + + // Subsequent calls should not reach native. + mOtpVerificationDialogBridge.onConfirm("123456"); + mOtpVerificationDialogBridge.onNewOtpRequested(); + mOtpVerificationDialogBridge.onDialogDismissed(); + + verify(mNativeMock, never()).onConfirm(anyLong(), anyString()); + verify(mNativeMock, never()).onNewOtpRequested(anyLong()); + verify(mNativeMock, times(1)).onDialogDismissed(anyLong()); + } + + @Test + @SmallTest + public void testNativeCallsDoNotOccurAfterDismissed() { + mOtpVerificationDialogBridge.onDialogDismissed(); + + verify(mNativeMock).onDialogDismissed(NATIVE_OTP_VERIFICATION_DIALOG_VIEW); + + mOtpVerificationDialogBridge.onConfirm("123456"); + mOtpVerificationDialogBridge.onNewOtpRequested(); + + verify(mNativeMock, never()).onConfirm(anyLong(), anyString()); + verify(mNativeMock, never()).onNewOtpRequested(anyLong()); + } +}
Original Bug Report
Potential Use-After-Free in OtpVerificationDialogViewAndroid during dismissal animation
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.
Overview: A potential Use-After-Free (UAF) exists in the Android OTP verification dialog. The native C++ object is destroyed synchronously upon dismissal, but the UI remains interactive during a 200ms fade-out animation. Interactions during this window invoke JNI methods using a stale pointer, potentially leading to Remote Code Execution in the browser process.
Affected files:
chrome/browser/ui/android/autofill/otp_verification_dialog_view_android.ccchrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogBridge.javachrome/browser/ui/android/autofill/internal/java/src/org/chromium/chrome/browser/ui/autofill/OtpVerificationDialogMediator.javacomponents/browser_ui/modaldialog/android/java/src/org/chromium/components/browser_ui/modaldialog/TabModalPresenter.javaui/android/java/src/org/chromium/ui/modaldialog/ModalDialogManager.java
Estimated timestamp from git blame: 2021-12-07
Summary
A potential Use-After-Free (UAF) vulnerability has been identified in the OTP verification dialog for Autofill Payments on Chrome for Android. The issue stems from a lifecycle mismatch: the native OtpVerificationDialogViewAndroid C++ object deletes itself synchronously when the dialog is dismissed, but the corresponding Android UI remains visible and interactive during a 200ms exit animation.
Because the native pointer is stored as a primitive long in the Java bridge (OtpVerificationDialogBridge), it is not protected by MiraclePtr (BackupRefPtr). User or attacker-induced interaction with the UI during the animation window triggers JNI calls on the freed memory, which could potentially be exploited to achieve Remote Code Execution (RCE) in the highly privileged browser process.
Technical Details
- Dialog Initialization: When the OTP dialog is created,
OtpVerificationDialogBridge(Java) receives a raw pointer toOtpVerificationDialogViewAndroid(C++) and stores it in a primitivelongfield namedmNativeOtpVerificationDialogView. - Synchronous Destruction: When the dialog is dismissed (e.g., via the Cancel button or programmatically),
ModalDialogManager.dismissDialog()synchronously triggers the controller’sonDismisscallback. This propagates to the Java bridge, which calls the native JNI methodonDialogDismissed. The C++ methodOtpVerificationDialogViewAndroid::OnDialogDismissedexecutesdelete this;, freeing the native object’s memory. - Asynchronous UI Teardown: Immediately after notifying the controller,
ModalDialogManagerinstructsTabModalPresenterto remove the view. Because the view is attached,TabModalPresenterinitiates a 200ms alpha fade-out animation (runExitAnimation). During this 200ms window, the view is not hidden, and its click listeners (such as theChromeClickableSpanfor the “Get new code” link) are not disabled. - Triggering the UAF: If an interaction occurs on the fading UI during this 200ms window (e.g., tapping “Get new code”), the Java bridge blindly passes the dangling
mNativeOtpVerificationDialogViewpointer across JNI toOtpVerificationDialogViewAndroid::OnNewOtpRequested. - Exploitation Path: Because the pointer crosses JNI as a primitive integer, MiraclePtr does not track it. An attacker capable of spraying the browser process heap (e.g., from a compromised renderer) could predictably reclaim the freed memory during the 200ms window. The C++ code evaluates
if (controller_)wherecontroller_is abase::WeakPtr. The attacker can forge theWeakPtrand its internalFlagobject to pass the validity check. The subsequent call to the pure virtual methodcontroller_->OnNewCodeLinkClicked()reads a fake vtable from the attacker-controlled memory, leading to potential arbitrary control-flow hijacking.
Note: These steps describe a potential exploitation path based on code analysis; an active proof-of-concept exploit has not been executed.
Potential Reproduction Steps
- Trigger an OTP verification dialog in Chrome for Android.
- Initiate dismissal of the dialog (e.g., tap ‘Cancel’).
- Within the 200ms exit animation window, tap the ‘Get new code’ link or the ‘Confirm’ button.
- Observe a crash or unexpected behavior due to the use of the freed native object.
Proposed Fix
The Java OtpVerificationDialogBridge should clear its native pointer upon dismissal to prevent further JNI calls. Specifically, in OtpVerificationDialogBridge.java:
@Override
public void onDialogDismissed() {
if (mNativeOtpVerificationDialogView == 0) return;
OtpVerificationDialogBridgeJni.get().onDialogDismissed(mNativeOtpVerificationDialogView);
// Add this line to clear the pointer:
mNativeOtpVerificationDialogView = 0;
}
All other methods in OtpVerificationDialogBridge that call into native (e.g., onConfirm, onNewOtpRequested) should check if mNativeOtpVerificationDialogView != 0 before proceeding. Additionally, TabModalPresenter should consider disabling interaction (e.g., via setClickable(false)) on the dialog container immediately when an exit animation begins.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.