CVE-2026-11108
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java |
modified |
Files Changed
services/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.javaservices/device/nfc/android/junit/src/org/chromium/device/nfc/NFCTest.java
Patch
From a5ee1692c4fca22cd37a7adb0b9c22dd9464ca3b Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> Date: Thu, 30 Apr 2026 15:39:06 -0700 Subject: [PATCH] [Web NFC] Fix visibility gate bypass in NfcImpl This CL fixes a logic error in NfcImpl.java where push and makeReadOnly operations were allowed to proceed even when NFC operations were suspended (e.g., when the tab was hidden). The fix adds missing return statements in push() and makeReadOnly() when mOperationsSuspended is true, and adds defensive checks in the processing methods to ensure operations are not executed against a physical tag if the tab has become hidden. Updated NFCTest.java to verify that no tag operations occur when suspended. Fixed: 500517053 Change-Id: Ia97f2b0e890ac6fc63bebbc024b6c5703177b147 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7807188 Reviewed-by: Reilly Grant <[email protected]> Commit-Queue: Andrew Paseltiner <[email protected]> Cr-Commit-Position: refs/heads/main@{#1623538} --- diff --git a/services/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java b/services/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java index 143d4f54..bcb1857a 100644 --- a/services/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java +++ b/services/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java @@ -210,6 +210,7 @@ createError( NdefErrorType.OPERATION_CANCELLED, "Cannot push the message because NFC operations are suspended.")); + return; } if (!NdefMessageValidator.isValid(message)) { @@ -259,6 +260,7 @@ createError( NdefErrorType.OPERATION_CANCELLED, "Cannot make read-only because NFC operations are suspended.")); + return; } // If previous pending make read-only operation is not completed, cancel it. @@ -525,7 +527,7 @@ * exception calls pendingPushOperationCompleted() with appropriate error object. */ private void processPendingPushOperation() { - if (mTagHandler == null || mPendingPushOperation == null) return; + if (mTagHandler == null || mPendingPushOperation == null || mOperationsSuspended) return; if (mTagHandler.isTagOutOfRange()) { mTagHandler = null; @@ -591,7 +593,9 @@ * of exception calls pendingMakeReadOnlyOperationCompleted() with appropriate error object. */ private void processPendingMakeReadOnlyOperation() { - if (mTagHandler == null || mPendingMakeReadOnlyOperation == null) return; + if (mTagHandler == null || mPendingMakeReadOnlyOperation == null || mOperationsSuspended) { + return; + } if (mTagHandler.isTagOutOfRange()) { mTagHandler = null; diff --git a/services/device/nfc/android/junit/src/org/chromium/device/nfc/NFCTest.java b/services/device/nfc/android/junit/src/org/chromium/device/nfc/NFCTest.java index afdb012d..4b46ebd 100644 --- a/services/device/nfc/android/junit/src/org/chromium/device/nfc/NFCTest.java +++ b/services/device/nfc/android/junit/src/org/chromium/device/nfc/NFCTest.java @@ -17,6 +17,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -1197,7 +1198,7 @@ /** Test that Nfc.push() fails if NFC operations are already suspended. */ @Test @Feature({"NFCTest"}) - public void testPushWhenOperationsAreSuspended() { + public void testPushWhenOperationsAreSuspended() throws IOException, FormatException { TestNfcImpl nfc = new TestNfcImpl(mContext, mDelegate); nfc.suspendNfcOperations(); mDelegate.invokeCallback(); @@ -1208,6 +1209,10 @@ verify(mockCallback).call(mErrorCaptor.capture()); assertNotNull(mErrorCaptor.getValue()); assertEquals(NdefErrorType.OPERATION_CANCELLED, mErrorCaptor.getValue().errorType); + + // Check that push is not triggered when NFC tag is in proximity. + nfc.processPendingOperationsForTesting(mNfcTagHandler); + verify(mNfcTagHandler, never()).write(any(android.nfc.NdefMessage.class)); } /** Test that Nfc.suspendNfcOperations() cancels pending push operation. */ @@ -1257,7 +1262,7 @@ /** Test that Nfc.makeReadOnly() fails if NFC operations are already suspended. */ @Test @Feature({"NFCTest"}) - public void testMakeReadOnlyWhenOperationsAreSuspended() { + public void testMakeReadOnlyWhenOperationsAreSuspended() throws IOException { TestNfcImpl nfc = new TestNfcImpl(mContext, mDelegate); nfc.suspendNfcOperations(); mDelegate.invokeCallback(); @@ -1268,6 +1273,10 @@ verify(mockCallback).call(mErrorCaptor.capture()); assertNotNull(mErrorCaptor.getValue()); assertEquals(NdefErrorType.OPERATION_CANCELLED, mErrorCaptor.getValue().errorType); + + // Check that makeReadOnly is not triggered when NFC tag is in proximity. + nfc.processPendingOperationsForTesting(mNfcTagHandler); + verify(mNfcTagHandler, never()).makeReadOnly(); } /** Test that Nfc.suspendNfcOperations() cancels pending makeReadOnly operation. */
Original Bug Report
WebNFC Android: Missing return allows background tabs to silently write/lock NFC tags
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 security team.
Overview: A missing return statement in NfcImpl.java allows NFC push and makeReadOnly operations to proceed even when the tab is hidden and operations should be suspended. This bypasses the Web NFC visibility-gate security model, enabling a background tab to silently overwrite or permanently lock physical NFC tags. The physical modification occurs successfully despite a subsequent Mojo protocol error on the renderer side.
Affected files:
services/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java
Estimated timestamp from git blame: 2021-12-14
Background
Web NFC requires that NFC operations be suspended when the page is not visible to the user (e.g., when the tab is backgrounded). This is implemented in Chrome for Android using the mOperationsSuspended flag in NfcImpl.java.
Potential Vulnerability
A logic error exists in services/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java that allows a background tab to bypass this visibility gate. In both the push() (lines 208-213) and makeReadOnly() (lines 257-262) methods, there is a check for mOperationsSuspended:
if (mOperationsSuspended) {
callback.call(
createError(
NdefErrorType.OPERATION_CANCELLED,
"..."));
}
However, these blocks are missing a return statement. Consequently, execution falls through. The method proceeds to queue the operation in mPendingPushOperation or mPendingMakeReadOnlyOperation and calls enableReaderModeIfNeeded().
Because an operation is now pending, enableReaderModeIfNeeded() re-enables Android’s NFC reader mode, effectively reversing the suspension. When the user subsequently taps an NFC tag (even while interacting with a different foreground app), onTagDiscovered() triggers processPendingOperations(). The queued write or lock operation is then executed against the physical tag synchronously on the binder thread.
While the renderer receives the initial OPERATION_CANCELLED callback and rejects the JavaScript promise, it does not close the Mojo connection. The Java side then completes the physical write. The subsequent duplicate callback (sent after the successful write) causes a Mojo protocol error that drops the connection, but this occurs too late to prevent the physical modification of the NFC tag.
Potential Steps to Reproduce
Note: These steps are based on a theoretical code path trace.
- Attacker Setup: The attacker hosts a malicious website and tricks the user into visiting it in Chrome for Android on an NFC-enabled device.
- Permission Grant: The attacker’s script calls
NDEFReader.write()orNDEFReader.makeReadOnly()accompanied by a user gesture. The user grants the requested NFC permission. - Backgrounding: The user switches to another tab or minimizes Chrome, causing the attacker’s tab to become hidden. This triggers
NfcImpl.suspendNfcOperations(), settingmOperationsSuspended = true. - Attacker Action: While the tab is still hidden, the attacker’s script asynchronously calls
NDEFReader.write()(ormakeReadOnly()) with a malicious payload via a timer. - Blink Processing:
NDEFReader::write()in Blink checks for a top-level frame but does not check page visibility. The request is forwarded via Mojo to the browser process. - The Vulnerability Triggered: In
NfcImpl.push(), the code detectsmOperationsSuspendedand sends anOPERATION_CANCELLEDerror callback, but crucially fails toreturn. It queues the attacker’s payload and re-enables the physical NFC reader. - User Action: Sometime later, the user (likely interacting with a completely different app) taps an NFC tag against the device.
- Physical Tag Modification: Android’s NFC system detects the tag and triggers
NfcImpl.onTagDiscovered(). This sequentially processes pending operations, executingmTagHandler.write(...)to push the attacker’s queued message to the physical tag. The attacker has successfully written to (or locked) a physical NFC tag without the user’s knowledge, violating the Web NFC visibility-gate security model.
Suggested Fix
Add the missing return statements in the mOperationsSuspended check blocks within push() and makeReadOnly() in NfcImpl.java.
if (mOperationsSuspended) {
callback.call(
createError(
NdefErrorType.OPERATION_CANCELLED,
"Cannot push the message because NFC operations are suspended."));
return; // <-- Missing return statement
}
Furthermore, for defense-in-depth, processPendingPushOperation() and processPendingMakeReadOnlyOperation() should be updated to explicitly check mOperationsSuspended before proceeding with the physical write, consistent with how processPendingWatchOperations() handles suspension.
Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234
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.