CVE-2026-11108
Overview
Background
- Web NFC
- Web platform API that lets a page read from and write to NFC tags; write actions include
push(write anNDEFMessage) andmakeReadOnly(permanently lock a tag). - Visibility gate
- Security requirement that NFC operations only proceed while the page is visible/focused; when a tab is hidden the renderer suspends operations and sets
mOperationsSuspended=true. - `NfcImpl`
- Android browser-process implementation backing the Web NFC Mojo interface, holding pending operations (
mPendingPushOperation) and the activemTagHandlerand dispatching them when a tag comes into range. - `OPERATION_CANCELLED`
NdefErrorTypereturned to the renderer to signal a rejected/suspended operation.
Root Cause Analysis
In NfcImpl.push() and NfcImpl.makeReadOnly(), the suspended-state branch constructed and emitted the OPERATION_CANCELLED error but omitted a return, so control fell through and the operation was still stored as the pending push/make-read-only. The dispatch methods processPendingPushOperation() and processPendingMakeReadOnlyOperation() gated only on mTagHandler and the pending-operation fields, never consulting mOperationsSuspended.
As a result, when a physical tag later came into range, the queued write or lock was executed even though the page was backgrounded and had already been told the operation was cancelled. The reject-and-continue pattern gave a false negative to the renderer while silently keeping the operation live, converting a UI-visibility guard into a no-op at the point that matters.
The fix adds the missing return; statements and defensively re-checks mOperationsSuspended in both processing methods.
return, the suspended state produced a cancellation message yet left the operation queued to fire on the next tag. Defense in depth requires the dispatch path to independently honor mOperationsSuspended, not trust that callers bailed out.Attack Path
- Stage a write
A malicious or backgrounded page holding an NFC grant calls
push()ormakeReadOnly()while its tab is hidden and operations are suspended. - False cancellation
NfcImplreturnsOPERATION_CANCELLEDto the renderer but, missing thereturn, still records the pending operation. - Ungated dispatch
When a physical tag enters range,
processPendingPushOperation()/processPendingMakeReadOnlyOperation()run without checkingmOperationsSuspended. - Silent tag mutation The tag is written to or permanently locked despite the page being hidden, bypassing the visibility gate and potentially damaging or hijacking the tag’s contents.
Impact Assessment
NDEFMessage data to, or irreversibly lock, any NFC tag brought into range, defeating the visibility gate meant to ensure the user is actively interacting. makeReadOnly is destructive and non-reversible, so the abuse can permanently brick a tag. Exploitation is bounded by requiring a prior NFC grant and physical tag proximity, limiting scope but not the integrity impact once those hold.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
Audit Directions
- Reject-without-returnSearch error/guard branches that build and emit an error (
OPERATION_CANCELLED, reject callbacks) but lack a followingreturn, leaving the operation queued or executed. - State checks split from dispatchWhere a precondition like
mOperationsSuspendedis enforced only at enqueue time, verify the deferred dispatch path re-checks it, since the guarding state can change between queue and execution. - Destructive deferred operationsAudit lifecycle transitions (visibility, focus, permission revocation) against irreversible actions such as
makeReadOnlyto ensure pending operations are flushed or invalidated on suspend.
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.