CVE-2026-13887
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/browser/android/nfc_host_unittest.cc |
modified |
Files Changed
content/browser/android/nfc_host.cccontent/browser/android/nfc_host_unittest.cc
Patch
From e2f73644a9273ac70be414b6db262973a9aa4b67 Mon Sep 17 00:00:00 2001 From: Hongchan Choi <[email protected]> Date: Wed, 27 May 2026 11:22:23 -0700 Subject: [PATCH] [NFC] Verify requesting frame is primary main frame in Android NFCHost This CL adds a lifecycle check to NFCHost::GetNFC on Android to ensure the requesting RenderFrameHost matches web_contents()->GetPrimaryMainFrame(). This prevents BFCached or backgrounded frames from performing unauthorized cross-origin NFC operations using the foreground Activity. If the check fails, the renderer is terminated using a Mojo bad message. A new unit test is added to verify this security check. Bug: 500508524 Test: content_unittests --gtest_filter=NFCHostTest.* Change-Id: Iba6bc8ed668f1f842d7fd1be53e683bf8e862b42 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7870447 Reviewed-by: Nasko Oskov <[email protected]> Commit-Queue: Hongchan Choi <[email protected]> Reviewed-by: Alvin Ji <[email protected]> Cr-Commit-Position: refs/heads/main@{#1637108} --- diff --git a/content/browser/android/nfc_host.cc b/content/browser/android/nfc_host.cc index 22a3142..54a5dc3 100644 --- a/content/browser/android/nfc_host.cc +++ b/content/browser/android/nfc_host.cc @@ -60,6 +60,11 @@ return; } + if (web_contents()->GetPrimaryMainFrame() != render_frame_host) { + mojo::ReportBadMessage("WebNFC not on primary main frame."); + return; + } + if (render_frame_host->GetBrowserContext() ->GetPermissionController() ->GetPermissionStatusForCurrentDocument( diff --git a/content/browser/android/nfc_host_unittest.cc b/content/browser/android/nfc_host_unittest.cc index 3b7ae85..c549218 100644 --- a/content/browser/android/nfc_host_unittest.cc +++ b/content/browser/android/nfc_host_unittest.cc @@ -14,6 +14,8 @@ #include "content/test/test_render_view_host.h" #include "content/test/test_web_contents.h" #include "services/device/public/mojom/nfc.mojom.h" +#include "mojo/public/cpp/test_support/fake_message_dispatch_context.h" +#include "mojo/public/cpp/test_support/test_utils.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/permissions/permission_utils.h" @@ -97,4 +99,24 @@ DeleteContents(); } +TEST_F(NFCHostTest, GetNFCFromDifferentWebContentsMainFrame) { + NavigateAndCommit(GURL(kTestUrl)); + + // Create a second WebContents. + std::unique_ptr<TestWebContents> second_web_contents = + TestWebContents::Create(browser_context(), nullptr); + + mojo::Remote<device::mojom::NFC> nfc; + mojo::FakeMessageDispatchContext fake_dispatch_context; + mojo::test::BadMessageObserver bad_message_observer; + + // Request NFC from the first WebContents, but passing the main frame of the + // second WebContents. + contents()->GetNFC(second_web_contents->GetPrimaryMainFrame(), + nfc.BindNewPipeAndPassReceiver()); + + EXPECT_EQ("WebNFC not on primary main frame.", + bad_message_observer.WaitForBadMessage()); +} + } // namespace content
Regression Test / PoC
diff --git a/content/browser/android/nfc_host_unittest.cc b/content/browser/android/nfc_host_unittest.cc
index 3b7ae85..c549218 100644
--- a/content/browser/android/nfc_host_unittest.cc
+++ b/content/browser/android/nfc_host_unittest.cc
@@ -14,6 +14,8 @@
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "services/device/public/mojom/nfc.mojom.h"
+#include "mojo/public/cpp/test_support/fake_message_dispatch_context.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
@@ -97,4 +99,24 @@
DeleteContents();
}
+TEST_F(NFCHostTest, GetNFCFromDifferentWebContentsMainFrame) {
+ NavigateAndCommit(GURL(kTestUrl));
+
+ // Create a second WebContents.
+ std::unique_ptr<TestWebContents> second_web_contents =
+ TestWebContents::Create(browser_context(), nullptr);
+
+ mojo::Remote<device::mojom::NFC> nfc;
+ mojo::FakeMessageDispatchContext fake_dispatch_context;
+ mojo::test::BadMessageObserver bad_message_observer;
+
+ // Request NFC from the first WebContents, but passing the main frame of the
+ // second WebContents.
+ contents()->GetNFC(second_web_contents->GetPrimaryMainFrame(),
+ nfc.BindNewPipeAndPassReceiver());
+
+ EXPECT_EQ("WebNFC not on primary main frame.",
+ bad_message_observer.WaitForBadMessage());
+}
+
} // namespace content
Original Bug Report
Potential Cross-Origin NFC Access from BFCache via Android NFCHost
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 lifecycle check in the Android NFCHost implementation allows a compromised renderer in the Back/Forward Cache to re-bind the NFC interface. By leveraging the visibility of the active tab, the background page can enable NFC reader mode using the foreground Activity and silently intercept NFC tag data intended for a different origin.
Affected files:
content/browser/android/nfc_host.ccservices/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.javacontent/browser/permissions/permission_controller_impl.cc
Estimated timestamp from git blame: 2021-12-09
Description
A logic error in the Android NFCHost implementation allows a RenderFrameHost in the Back/Forward Cache (BFCache) to access the device.mojom.NFC service.
When a frame requests the NFC interface, NFCHost::GetNFC in content/browser/android/nfc_host.cc performs several lifecycle checks. It verifies that the frame has no parent, is not prerendering, and is not a fenced frame. However, it lacks a check to ensure that the requesting frame is the active primary main frame. This safeguard is correctly implemented in the equivalent iOS code (content/browser/ios/nfc_host.mm), which checks web_contents()->GetPrimaryMainFrame() != render_frame_host.
Because of this missing check on Android, a compromised renderer that has entered the BFCache can bypass renderer-side suspension and send a new Mojo request for device.mojom.NFC via its BrowserInterfaceBroker. NFCHost::GetNFC will accept the request and bind a new NFC provider pipe.
Subsequently, NFCHost::GetNFC calls MaybeResumeOrSuspendOperations(web_contents()->GetVisibility()). Because it checks the visibility of the entire WebContents (the tab), and the tab is actively displaying a different page, it returns Visibility::VISIBLE. This prevents the Java implementation from suspending operations.
On the Java side, when the BFCached page initiates a Watch operation, NfcHost.java retrieves the Android Activity associated with the active WebContents. This foreground Activity is then used to enable Android’s NFC Reader Mode. The Android OS, seeing a foreground Activity requesting NFC, allows the operation. When the user taps an NFC tag, the data is delivered to the backgrounded, BFCached page’s origin, resulting in a silent cross-origin data leak.
Impact
An attacker with a compromised renderer can perform cross-origin NFC operations while the user is interacting with a different, potentially sensitive site. A BFCached malicious page can:
- Read the contents of NFC tags (such as payment cards, access badges, or transit passes) when the user taps them while viewing a different site.
- Write attacker-controlled NDEF records to a tag.
- Permanently and irreversibly lock a tag using the
MakeReadOnlyoperation.
There is no system-level UI attribution indicating which origin is accessing the NFC hardware, meaning the user assumes the visible site is interacting with the tag.
Potential Reproduction Steps
Note: These are suggested steps based on code analysis.
- On an Android device, a user navigates to
https://evil.comand grants the site NFC permissions. - The user navigates to another site (e.g.,
https://bank.com) in the same tab.evil.comenters the BFCache. - A compromised renderer for
evil.combypasses freezing and sends a Mojo request fordevice.mojom.NFCvia itsBrowserInterfaceBrokerpipe. - The browser process binds the NFC interface because it passes the incomplete lifecycle checks in
content/browser/android/nfc_host.cc. - The
evil.compage starts an NFCwatchoperation. Because the tab is visible,NfcImpl.javasuccessfully uses the foreground Activity to enable NFC Reader Mode. - The user taps an NFC tag while viewing
bank.com. The tag data is silently delivered to the BFCachedevil.comrenderer.
Suggested Fix
Update NFCHost::GetNFC in content/browser/android/nfc_host.cc to verify that the requesting frame is the primary main frame, bringing it inline with the iOS implementation:
if (web_contents()->GetPrimaryMainFrame() != render_frame_host) {
mojo::ReportBadMessage("WebNFC not on primary main frame.");
return;
}
Alternatively, add an explicit check to reject requests if the frame’s lifecycle state is RenderFrameHost::LifecycleState::kInBackForwardCache.
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.