Medium watch CVSS 6.5 chrome Policy Bypass 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in NFC
DescriptionInsufficient policy enforcement in NFC
ComponentNFC
Bug ClassPolicy Bypass
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker500508524
Fix commite2f73644a927 (chromium/src) +27/-0
CWECWE-346, CWE-352 (Origin validation error, CSRF)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-08 (per vendor advisory)
Fix landed2026-05-27 (public git)
Disclosed2026-06-30
Patch gap34 days — fix public in source before the advisory

Background

WebNFC
Web platform API exposing NFC read/write to pages, gated to top-level, foreground, focused contexts by the spec.
`content::NFCHost`
Browser-process object that brokers blink.mojom.NFC interface requests from a renderer’s RenderFrameHost to the platform NFC adapter.
Primary main frame
The current top-level RenderFrameHost of a WebContents; excludes BFCached, prerendered, subframe, and detached frames.
`mojo::ReportBadMessage`
Mojo-layer primitive that flags a renderer IPC as invalid and terminates the offending renderer process.

Root Cause Analysis

NFCHost::GetNFC bound the blink.mojom.NFC interface for whatever RenderFrameHost issued the request, verifying only origin/permission state and never confirming the frame was the current primary main frame. Because a back/forward-cached or backgrounded frame retains a live RenderFrameHost and can still dispatch Mojo IPCs, such a frame could invoke GetNFC and be handed an NFC binding despite no longer being top-level or foreground. The browser then serviced NFC operations against the foreground Activity’s single NFC adapter on behalf of a non-foreground, potentially cross-origin context, violating the WebNFC “primary main frame only” invariant. The defect is a missing lifecycle/state check on a security-relevant capability grant — a logic error, not memory unsafety.

The fix adds web_contents()->GetPrimaryMainFrame() != render_frame_host as a hard gate and treats a mismatch as a malformed message worthy of renderer termination.

Key insight
A live RenderFrameHost is not proof of foreground primacy: BFCached and backgrounded frames keep valid Mojo endpoints and can request capabilities long after they cease to be the primary main frame. Capability brokers must re-validate frame lifecycle state at bind time, never trusting that only the current top frame can reach the endpoint.

Attack Path

  1. Acquire a stale frame Attacker page navigates such that its RenderFrameHost enters the back/forward cache or is backgrounded while remaining alive.
  2. Replay the request The stale renderer sends a GetNFC Mojo message from the non-primary RenderFrameHost.
  3. Bind without check Pre-fix NFCHost::GetNFC binds the NFC interface, passing only the origin/permission check.
  4. Operate on foreground adapter The cross-frame/cross-origin context drives NFC reads/writes against the current foreground Activity’s adapter, unauthorized.

Impact Assessment

A backgrounded or BFCached, potentially cross-origin frame gains unauthorized NFC read/write access using the foreground Activity’s adapter, defeating the WebNFC same-context and foreground requirements. This enables covert reading of nearby tags or writing attacker-controlled records without the user associating the action with the visible foreground page. Severity is bounded by NFC availability and the existing permission prompt, but the policy-enforcement gap is the core defect.

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

Audit Directions

  • Bind-time lifecycle validation
    Audit other browser-process capability brokers (Get*/interface binders) that trust a RenderFrameHost without comparing against GetPrimaryMainFrame() or checking IsInPrimaryMainFrame()/GetLifecycleState().
  • BFCache-reachable Mojo endpoints
    Enumerate blink.mojom interfaces bound per-frame and confirm each rejects or defers requests from cached, prerendered, or backgrounded frames.
  • Cross-WebContents capability leakage
    Review platform singletons (NFC, sensors, media) shared with the foreground Activity to ensure the requesting frame belongs to the foreground WebContents, not merely to a live renderer.
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