Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Geolocation
DescriptionInappropriate implementation in Geolocation
ComponentGeolocation
Bug ClassLogic Error
Tracker497345177
Fix commit40d7259ff270 (chromium/src) +47/-48
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
InstalledWebappGeolocationContext
chrome/browser/webapps/installable/installed_webapp_geolocation_bridge.h
modified
erase_if
chrome/browser/webapps/installable/installed_webapp_geolocation_context.cc
modified
if
components/omnibox/browser/geolocation_header_service.cc
modified
if
services/device/geolocation/geolocation_context.cc
modified
erase_if
services/device/geolocation/geolocation_context.cc
modified

Files Changed

  • chrome/browser/webapps/installable/installed_webapp_geolocation_bridge.cc
  • chrome/browser/webapps/installable/installed_webapp_geolocation_bridge.h
  • chrome/browser/webapps/installable/installed_webapp_geolocation_context.cc
  • chrome/browser/webapps/installable/installed_webapp_geolocation_context.h
  • components/omnibox/browser/geolocation_header_service.cc
  • content/browser/geolocation/geolocation_service_impl.cc
  • services/device/geolocation/geolocation_context.cc
  • services/device/geolocation/geolocation_context.h
From 40d7259ff270bc0487063445617320cf2e6126da Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Tue, 12 May 2026 10:29:36 -0700
Subject: [PATCH] Fix Geolocation permission revocation bypass for about:blank popups

Track active geolocation streams by `url::Origin` instead of `GURL` in
the device service.

Previously, `about:blank` popups bypassed geolocation permission
revocation because the service tracked streams by their URL string
(`about:blank`) rather than their inherited origin. Upon permission
revocation, origin matching failed against the popup's opaque-converted
URL, leaving the stream active.

This CL updates `GeolocationContext::BindGeolocation` and
`GeolocationImpl` to use `url::Origin`, ensuring streams from
`about:blank` popups are correctly closed when the parent origin's
permission is revoked. Also updates relevant callers, components, and
test fakes.

Change-Id: Icec774c28c3fd88a1311a9072b67fa6b5c295c45
BUG: 497345177
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7824224
Reviewed-by: Orin Jaworski <orinj@chromium.org>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Reviewed-by: Dibyajyoti Pal <dibyapal@chromium.org>
Reviewed-by: Tom Sepez <tsepez@chromium.org>
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1629437}
---

diff --git a/chrome/browser/webapps/installable/installed_webapp_geolocation_bridge.cc b/chrome/browser/webapps/installable/installed_webapp_geolocation_bridge.cc
index d4ffe58..72c4615 100644
--- a/chrome/browser/webapps/installable/installed_webapp_geolocation_bridge.cc
+++ b/chrome/browser/webapps/installable/installed_webapp_geolocation_bridge.cc
@@ -21,10 +21,10 @@
 
 InstalledWebappGeolocationBridge::InstalledWebappGeolocationBridge(
     mojo::PendingReceiver<Geolocation> receiver,
-    const GURL& url,
+    const url::Origin& origin,
     InstalledWebappGeolocationContext* context)
     : context_(context),
-      url_(url),
+      origin_(origin),
       high_accuracy_(false),
       receiver_(this, std::move(receiver)) {
   DCHECK(context_);
@@ -42,7 +42,7 @@
   if (java_ref_.is_null()) {
     java_ref_.Reset(InstalledWebappGeolocationBridgeJni::create(
         env, reinterpret_cast<intptr_t>(this),
-        url::GURLAndroid::FromNativeGURL(env, url_)));
+        url::GURLAndroid::FromNativeGURL(env, origin_.GetURL())));
   }
   java_ref_->start(env, high_accuracy_);
 }
diff --git a/chrome/browser/webapps/installable/installed_webapp_geolocation_bridge.h b/chrome/browser/webapps/installable/installed_webapp_geolocation_bridge.h
index e39edea9..755c38d 100644
--- a/chrome/browser/webapps/installable/installed_webapp_geolocation_bridge.h
+++ b/chrome/browser/webapps/installable/installed_webapp_geolocation_bridge.h
@@ -13,7 +13,7 @@
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "services/device/public/mojom/geolocation.mojom.h"
 #include "services/device/public/mojom/geoposition.mojom.h"
-#include "url/gurl.h"
+#include "url/origin.h"
 
 class InstalledWebappGeolocationContext;
 
@@ -23,7 +23,7 @@
   // |context| must outlive this object.
   InstalledWebappGeolocationBridge(
       mojo::PendingReceiver<device::mojom::Geolocation> receiver,
-      const GURL& origin,
+      const url::Origin& origin,
       InstalledWebappGeolocationContext* context);
   InstalledWebappGeolocationBridge(const InstalledWebappGeolocationBridge&) =
       delete;
@@ -57,7 +57,7 @@
                               double speed);
   void OnNewErrorAvailable(JNIEnv* env, const std::string& message);
 
-  const GURL& url() { return url_; }
+  const url::Origin& origin() const { return origin_; }
 
  private:
   // device::mojom::Geolocation:
@@ -82,7 +82,7 @@
 
   device::mojom::GeopositionResultPtr current_position_;
 
-  const GURL url_;
+  const url::Origin origin_;
 
   // Whether this instance is currently observing location updates with high
   // accuracy.
diff --git a/chrome/browser/webapps/installable/installed_webapp_geolocation_context.cc b/chrome/browser/webapps/installable/installed_webapp_geolocation_context.cc
index efd7372a..05e309c 100644
--- a/chrome/browser/webapps/installable/installed_webapp_geolocation_context.cc
+++ b/chrome/browser/webapps/installable/installed_webapp_geolocation_context.cc
@@ -18,11 +18,11 @@
 
 void InstalledWebappGeolocationContext::BindGeolocation(
     mojo::PendingReceiver<device::mojom::Geolocation> receiver,
-    const GURL& requesting_url,
+    const url::Origin& requesting_origin,
     device::mojom::GeolocationClientId client_id,
     bool has_precise_permission) {
   impls_.push_back(std::make_unique<InstalledWebappGeolocationBridge>(
-      std::move(receiver), requesting_url, this));
+      std::move(receiver), requesting_origin, this));
   if (geoposition_override_)
     impls_.back()->SetOverride(geoposition_override_.Clone());
   else
@@ -32,7 +32,7 @@
 void InstalledWebappGeolocationContext::OnPermissionRevoked(
     const url::Origin& origin) {
   std::erase_if(impls_, [&origin](const auto& impl) {
-    if (!origin.IsSameOriginWith(impl->url())) {
+    if (origin != impl->origin()) {
       return false;
     }
     // Invoke the position callback with kPermissionDenied before removing.
diff --git a/chrome/browser/webapps/installable/installed_webapp_geolocation_context.h b/chrome/browser/webapps/installable/installed_webapp_geolocation_context.h
index a321c4dc..669ff26 100644
--- a/chrome/browser/webapps/installable/installed_webapp_geolocation_context.h
+++ b/chrome/browser/webapps/installable/installed_webapp_geolocation_context.h
@@ -35,7 +35,7 @@
   // permission control), so the `has_precise_permission` parameter is ignored.
   void BindGeolocation(
       mojo::PendingReceiver<device::mojom::Geolocation> receiver,
-      const GURL& requesting_url,
+      const url::Origin& requesting_origin,
       device::mojom::GeolocationClientId client_id,
       bool has_precise_permission) override;
   void OnPermissionUpdated(
diff --git a/components/omnibox/browser/geolocation_header_service.cc b/components/omnibox/browser/geolocation_header_service.cc
index 8217fa5d..787fdd8 100644
--- a/components/omnibox/browser/geolocation_header_service.cc
+++ b/components/omnibox/browser/geolocation_header_service.cc
@@ -288,7 +288,8 @@
   // location prompt can attribute the location request to the correct origin.
   bool has_precise = HasPrecisePermission(requesting_url);
   geolocation_context_->BindGeolocation(
-      geolocation_.BindNewPipeAndPassReceiver(), requesting_url,
+      geolocation_.BindNewPipeAndPassReceiver(),
+      url::Origin::Create(requesting_url),
       device::mojom::GeolocationClientId::kOmnibox, has_precise);
 
   if (!use_cache_only) {
diff --git a/content/browser/geolocation/geolocation_service_impl.cc b/content/browser/geolocation/geolocation_service_impl.cc
index 3d2e5b2..312c511 100644
--- a/content/browser/geolocation/geolocation_service_impl.cc
+++ b/content/browser/geolocation/geolocation_service_impl.cc
@@ -185,7 +185,7 @@
   bool has_precise_permission =
       permission_level == GeolocationPermissionLevel::kPrecise;
   geolocation_context->BindGeolocation(
-      std::move(receiver), requesting_url,
+      std::move(receiver), requesting_origin_,
       device::mojom::GeolocationClientId::kGeolocationServiceImpl,
       has_precise_permission);
   subscription_id_ =
diff --git a/services/device/geolocation/geolocation_context.cc b/services/device/geolocation/geolocation_context.cc
index 27a2538..3fa0614 100644
--- a/services/device/geolocation/geolocation_context.cc
+++ b/services/device/geolocation/geolocation_context.cc
@@ -27,12 +27,12 @@
 
 void GeolocationContext::BindGeolocation(
     mojo::PendingReceiver<mojom::Geolocation> receiver,
-    const GURL& requesting_url,
+    const url::Origin& requesting_origin,
     mojom::GeolocationClientId client_id,
     bool has_precise_permission) {
   GeolocationImpl* impl =
-      new GeolocationImpl(std::move(receiver), requesting_url, client_id, this,
-                          has_precise_permission);
+      new GeolocationImpl(std::move(receiver), requesting_origin, client_id,
+                          this, has_precise_permission);
   impls_.push_back(base::WrapUnique<GeolocationImpl>(impl));
   if (geoposition_override_) {
     impl->SetOverride(*geoposition_override_);
@@ -45,7 +45,7 @@
     const url::Origin& origin,
     mojom::GeolocationPermissionLevel permission_level) {
   std::erase_if(impls_, [&origin, &permission_level](const auto& impl) {
-    if (!origin.IsSameOriginWith(impl->url())) {
+    if (origin != impl->origin()) {
       return false;
     }
     // Pass the permission update to the GeolocationImpl, and erase the impl if
diff --git a/services/device/geolocation/geolocation_context.h b/services/device/geolocation/geolocation_context.h
index f4eb656..e9205fe6 100644
--- a/services/device/geolocation/geolocation_context.h
+++ b/services/device/geolocation/geolocation_context.h
@@ -36,7 +36,7 @@
 
   // mojom::GeolocationContext implementation:
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/device/geolocation/geolocation_impl_unittest.cc b/services/device/geolocation/geolocation_impl_unittest.cc
index 4d63395..b34dea1 100644
--- a/services/device/geolocation/geolocation_impl_unittest.cc
+++ b/services/device/geolocation/geolocation_impl_unittest.cc
@@ -15,6 +15,8 @@
 #include "services/device/geolocation/geolocation_provider.h"
 #include "services/device/public/mojom/geolocation_client_id.mojom.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+#include "url/origin.h"
 
 namespace device {
 
@@ -87,7 +89,8 @@
   void BindGeolocation(bool has_precise_permission) {
     geolocation_.reset();
     geolocation_context_.BindGeolocation(
-        geolocation_.BindNewPipeAndPassReceiver(), GURL("https://test.com"),
+        geolocation_.BindNewPipeAndPassReceiver(),
+        url::Origin::Create(GURL("https://test.com")),
         mojom::GeolocationClientId::kForTesting,
         /*has_precise_permission=*/true);
   }
diff --git a/services/device/public/cpp/test/scoped_geolocation_overrider.cc b/services/device/public/cpp/test/scoped_geolocation_overrider.cc
index 675d921..2033b625f5 100644
--- a/services/device/public/cpp/test/scoped_geolocation_overrider.cc
+++ b/services/device/public/cpp/test/scoped_geolocation_overrider.cc
@@ -50,7 +50,7 @@
   // The `has_precise_permission` parameter is ignored as approximate │
   // geolocation is not yet supported by this fake test class.
   void BindGeolocation(mojo::PendingReceiver<mojom::Geolocation> receiver,
-                       const GURL& requesting_url,
+                       const url::Origin& requesting_origin,
                        mojom::GeolocationClientId client_id,
                        bool has_precise_permission) override;
   void OnPermissionUpdated(
@@ -94,7 +94,7 @@
 class ScopedGeolocationOverrider::FakeGeolocation : public mojom::Geolocation {
  public:
   FakeGeolocation(mojo::PendingReceiver<mojom::Geolocation> receiver,
-                  const GURL& requesting_url,
+                  const url::Origin& requesting_origin,
                   FakeGeolocationContext* context);
   ~FakeGeolocation() override;
 
@@ -108,12 +108,12 @@
   void QueryNextPosition(QueryNextPositionCallback callback) override;
   void QueryCachedPosition(QueryCachedPositionCallback callback) override;
   void SetHighAccuracyHint(bool high_accuracy) override;
-  const GURL& url() { return url_; }
+  const url::Origin& origin() const { return origin_; }
 
  private:
   void RunPositionCallbackIfNeeded();
 
-  const GURL url_;
+  const url::Origin origin_;
   raw_ptr<FakeGeolocationContext> context_;
   bool needs_update_ = true;
   bool high_accuracy_hint_ = false;
@@ -247,13 +247,13 @@
 
 void ScopedGeolocationOverrider::FakeGeolocationContext::BindGeolocation(
     mojo::PendingReceiver<mojom::Geolocation> receiver,
-    const GURL& requesting_url,
+    const url::Origin& requesting_origin,
     mojom::GeolocationClientId client_id,
     bool has_precise_permission) {
   // The `has_precise_permission` parameter is ignored as approximate
   // geolocation is not yet supported by this fake test class.
   impls_.insert(std::make_unique<FakeGeolocation>(std::move(receiver),
-                                                  requesting_url, this));
+                                                  requesting_origin, this));
 }
 
 void ScopedGeolocationOverrider::FakeGeolocationContext::OnPermissionUpdated(
@@ -263,7 +263,7 @@
   // should be updated to handle other permission levels if the fake needs to
   // support them.
   std::erase_if(impls_, [&origin, &permission_level](const auto& impl) {
-    if (!origin.IsSameOriginWith(impl->url())) {
+    if (origin != impl->origin()) {
       return false;
     }
     if (permission_level == mojom::GeolocationPermissionLevel::kDenied) {
@@ -314,9 +314,9 @@
 
 ScopedGeolocationOverrider::FakeGeolocation::FakeGeolocation(
     mojo::PendingReceiver<mojom::Geolocation> receiver,
-    const GURL& requesting_url,
+    const url::Origin& requesting_origin,
     FakeGeolocationContext* context)
-    : url_(requesting_url), context_(context) {
+    : origin_(requesting_origin), context_(context) {
   receiver_.Bind(std::move(receiver));
   receiver_.set_disconnect_handler(
       base::BindOnce(&ScopedGeolocationOverrider::FakeGeolocation::OnDisconnect,
Loading diff…

Original Bug Report

reported by vm...@google.com

Geolocation permission revocation bypass via about:blank popups

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A potential logic flaw allows about:blank popups to bypass geolocation permission revocation and continue receiving location updates. The service incorrectly uses the frame’s URL (about:blank) instead of its inherited origin when tracking active streams, causing origin-based revocation checks to fail. This allows an attacker to stealthily track the user after permission is revoked, as the browser’s UI tracking indicator is simultaneously dismissed.

Affected files:

  • content/browser/geolocation/geolocation_service_impl.cc
  • services/device/geolocation/geolocation_context.cc

Estimated timestamp from git blame: 2025-12-01

Description

A potential logic vulnerability exists in the browser’s Geolocation service that allows about:blank popups to maintain persistent access to a user’s location even after the user explicitly revokes the site’s geolocation permission. The bug arises from a mismatch between the URL used to register the geolocation stream and the origin used to revoke it.

Because the revocation also clears the location-tracking UI indicator (the crosshairs icon), the user is left with the false impression that location tracking has successfully stopped, making this a stealthy privacy bypass.

Technical Details

When a frame requests geolocation, GeolocationServiceImpl::CreateGeolocationWithPermissionResult (in content/browser/geolocation/geolocation_service_impl.cc) saves both the frame’s origin and URL:

  requesting_origin_ =
      render_frame_host_->GetMainFrame()->GetLastCommittedOrigin();
  auto requesting_url =
      render_frame_host_->GetMainFrame()->GetLastCommittedURL();

For an about:blank popup opened by https://example.com, requesting_origin_ is the inherited tuple origin https://example.com, but requesting_url is the GURL about:blank. The service binds the device stream by calling geolocation_context->BindGeolocation(..., requesting_url, ...). The resulting GeolocationImpl stores about:blank as its url_.

When the user revokes the permission for https://example.com, GeolocationServiceImpl::HandlePermissionResultChange correctly dismisses the UI tracking indicator by calling DecrementActivityCount(). It then attempts to close the active location stream by calling:

  geolocation_context->OnPermissionUpdated(requesting_origin_, permission_level);

In services/device/geolocation/geolocation_context.cc, OnPermissionUpdated tries to erase affected streams:

  std::erase_if(impls_, [&origin, &permission_level](const auto& impl) {
    if (!origin.IsSameOriginWith(impl->url())) {
      return false;
    }
    // ... notify and erase
  });

Here, it compares the tuple origin (https://example.com) against impl->url() (about:blank). The method url::Origin::IsSameOriginWith(const GURL&) converts the GURL to an origin to perform the comparison. Because about:blank uses a restricted scheme, it converts to a unique, opaque origin. A tuple origin is never same-origin with an opaque origin, so the check evaluates to false.

Consequently, the stream is never erased, and the popup continues to receive real-time location updates from the OS.

Suggested Steps to Reproduce

(Note: These are potential steps to trigger the vulnerability, as our tooling agent does not currently have the ability to run code to provide a working proof of concept.)

  1. An attacker hosts a malicious page at https://example.com and tricks the user into granting Geolocation permissions.
  2. The attacker’s page executes window.open("about:blank") to open a popup.
  3. The attacker injects a script into the popup: popup.document.write('<script>navigator.geolocation.watchPosition(pos => sendToAttacker(pos));</script>').
  4. The user notices the browser’s location tracking icon and revokes Geolocation permission for https://example.com via Chrome’s Site Settings.
  5. The browser’s tracking icon immediately disappears.
  6. Observe: The watchPosition callback inside the about:blank popup continues to fire, and the attacker continues receiving the user’s location.

Suggested Fix

The device service’s GeolocationContext should track streams by url::Origin rather than GURL.

  1. Update device::mojom::GeolocationContext::BindGeolocation to take a url.mojom.Origin instead of a url.mojom.Url. (Note: This aligns with an existing TODO in geolocation_context.mojom referencing crbug.com/453708846).
  2. Update GeolocationServiceImpl::CreateGeolocationWithPermissionResult to pass requesting_origin_ instead of requesting_url to BindGeolocation.
  3. Update GeolocationImpl to store a url::Origin as a class member and compare against it directly in OnPermissionUpdated.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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