Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in NFC
DescriptionInsufficient policy enforcement in NFC
ComponentNFC
Bug ClassLogic Error
Tracker500508524
Fix commite2f73644a927 (chromium/src) +27/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
TEST_F
content/browser/android/nfc_host_unittest.cc
modified

Files Changed

  • content/browser/android/nfc_host.cc
  • content/browser/android/nfc_host_unittest.cc
From e2f73644a9273ac70be414b6db262973a9aa4b67 Mon Sep 17 00:00:00 2001
From: Hongchan Choi <hongchan@google.com>
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 <nasko@chromium.org>
Commit-Queue: Hongchan Choi <hongchan@chromium.org>
Reviewed-by: Alvin Ji <alvinji@chromium.org>
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • services/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java
  • content/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:

  1. 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.
  2. Write attacker-controlled NDEF records to a tag.
  3. Permanently and irreversibly lock a tag using the MakeReadOnly operation.

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.

  1. On an Android device, a user navigates to https://evil.com and grants the site NFC permissions.
  2. The user navigates to another site (e.g., https://bank.com) in the same tab. evil.com enters the BFCache.
  3. A compromised renderer for evil.com bypasses freezing and sends a Mojo request for device.mojom.NFC via its BrowserInterfaceBroker pipe.
  4. The browser process binds the NFC interface because it passes the incomplete lifecycle checks in content/browser/android/nfc_host.cc.
  5. The evil.com page starts an NFC watch operation. Because the tab is visible, NfcImpl.java successfully uses the foreground Activity to enable NFC Reader Mode.
  6. The user taps an NFC tag while viewing bank.com. The tag data is silently delivered to the BFCached evil.com renderer.

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.

View on issue tracker