Medium watch CVSS 6.5 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Omnibox
DescriptionMissing authorization in Omnibox
ComponentOmnibox
Bug ClassAuthorization Error
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.
Tracker507225626
Fix commitee8d8601246e (chromium/src) +127/-1
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-28 (per vendor advisory)
Fix landed2026-07-29 (public git)
Disclosed2026-09-08
Patch gap41 days — fix public in source before the advisory

Background

`IsWebSafeScheme`
a ChildProcessSecurityPolicy predicate that returns true only for schemes a renderer/web content is allowed to navigate to (e.g. http, https), excluding privileged schemes like chrome: and file:.
`PasteAndGo` / middle-click paste
a location-bar affordance where a middle click pastes the selection-clipboard text and navigates to it as if typed into the omnibox.
`NewTabFromClipboardURL`
a browser command that classifies clipboard text into an AutocompleteMatch and opens its destination_url in a new tab.
`AutocompleteClassifier::Classify`
turns arbitrary text into an AutocompleteMatch with a destination_url, which may resolve to any scheme including privileged ones.

Root Cause Analysis

Both the NewTabFromClipboardURL command in browser_commands.cc and the middle-click paste-and-go path in location_bar_view.cc took clipboard text, classified it into an AutocompleteMatch, and navigated to match.destination_url after only checking is_valid() (or nothing at all in the paste-and-go path). Because clipboard content is externally controllable and the classifier can produce privileged-scheme URLs, this let a chrome:// or file:// navigation be initiated from a user gesture that was never intended to reach privileged schemes, bypassing the authorization boundary the omnibox normally enforces.

The fix tightens both paths: browser_commands.cc now additionally requires ChildProcessSecurityPolicy::GetInstance()->IsWebSafeScheme(match.destination_url.scheme()) before calling AddTabAt, and location_bar_view.cc classifies the pasted text and returns early if the resulting destination_url.scheme() is not web-safe, before reaching PasteAndGo. This works by admitting only web-safe schemes, so file:/chrome: and similar are rejected before any navigation is initiated.

Key insight
The core mistake was treating externally-controllable clipboard text as a trusted URL and navigating to it with only a validity check, never a scheme-authorization check; the fix gates both entry points on IsWebSafeScheme so unsafe schemes are dropped before navigation.

Attack Path

  1. Plant a privileged-scheme URL An attacker gets a URL such as file:///etc/passwd or chrome://version into the selection/primary clipboard (e.g. via a page that sets the selection).
  2. Induce the gesture The user performs a middle-click paste on the location-bar icon (OnMousePressed) or triggers the NewTabFromClipboardURL command.
  3. Unchecked classification The pre-patch code classifies the clipboard text into an AutocompleteMatch and proceeds to PasteAndGo/AddTabAt on destination_url without verifying the scheme.
  4. Privileged navigation The browser opens the chrome:// or file:// URL from this UI-initiated flow, reaching content that a web-triggered navigation should not be able to reach (potential local-file or privileged-page access is (inferred)).

Impact Assessment

An attacker who can control the selection clipboard and induce a middle-click paste or clipboard-URL new-tab action can cause the browser to navigate to privileged chrome:// or local file:// URLs that are normally off-limits to web-initiated navigation, in the browser UI context. The advisory classifies this as missing authorization in Omnibox; whether this yields concrete local-file disclosure or privileged-page access beyond the navigation itself is (inferred). The precondition is user interaction (a middle click or the clipboard-URL command) plus attacker-influenced clipboard contents; the regression tests confirm file:// and chrome:// are now rejected while http still opens.

Files Changed

  • chrome/browser/ui/views/location_bar/location_bar_view_browsertest.cc
  • chrome/browser/ui/browser_commands_browsertest.cc
  • chrome/browser/ui/views/location_bar/location_bar_view.cc
  • chrome/browser/ui/browser_commands.cc

Audit Directions

  • Clipboard-to-navigation flows
    Audit any path that turns clipboard/selection text into a navigation (paste-and-go, drag-and-drop, clipboard-URL commands) and confirm scheme authorization, not just URL validity, is enforced.
  • `AutocompleteMatch::destination_url` consumers
    Review callers that navigate to a classifier-produced destination_url and check each for an IsWebSafeScheme/blocklist gate before initiating navigation.
  • `is_valid()` used as a security check
    Flag places where GURL::is_valid() is treated as sufficient authorization; validity says nothing about whether the scheme is web-safe.
From ee8d8601246e5075b391cfe76f586f850fc0da44 Mon Sep 17 00:00:00 2001
From: Jan Keitel <jkeitel@google.com>
Date: Wed, 29 Jul 2026 04:06:50 -0700
Subject: [PATCH] Only allow safe schemes when opening from middle click

Bug: 507225626
Change-Id: I83580bf4b7d94a084340c4a03f85512db0655cb3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8118569
Commit-Queue: Jan Keitel <jkeitel@google.com>
Reviewed-by: Kunal Daftari <kunaldaftari@google.com>
Reviewed-by: David Pennington <dpenning@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1670118}
---
diff --git a/chrome/browser/ui/browser_commands.cc b/chrome/browser/ui/browser_commands.cc
index d6e1f1f8..67acdf3 100644
--- a/chrome/browser/ui/browser_commands.cc
+++ b/chrome/browser/ui/browser_commands.cc
@@ -182,6 +182,7 @@
 #include "components/zoom/page_zoom.h"
 #include "components/zoom/zoom_controller.h"
 #include "content/public/browser/browsing_data_remover.h"
+#include "content/public/browser/child_process_security_policy.h"
 #include "content/public/browser/devtools_agent_host.h"
 #include "content/public/browser/navigation_controller.h"
 #include "content/public/browser/navigation_entry.h"
@@ -1335,7 +1336,10 @@
                   ->Classify(text, false, false,
                              metrics::OmniboxEventProto::BLANK, &match,
                              nullptr);
-              if (match.destination_url.is_valid()) {
+              if (match.destination_url.is_valid() &&
+                  content::ChildProcessSecurityPolicy::GetInstance()
+                      ->IsWebSafeScheme(
+                          std::string(match.destination_url.scheme()))) {
                 browser_weak->tab_strip_model()->delegate()->AddTabAt(
                     match.destination_url, -1, true);
               }
diff --git a/chrome/browser/ui/views/location_bar/location_bar_view.cc b/chrome/browser/ui/views/location_bar/location_bar_view.cc
index c101d301..c7f9dad1 100644
--- a/chrome/browser/ui/views/location_bar/location_bar_view.cc
+++ b/chrome/browser/ui/views/location_bar/location_bar_view.cc
@@ -19,6 +19,7 @@
 #include "chrome/app/vector_icons/vector_icons.h"
 #include "chrome/browser/actor/ui/actor_ui_window_controller.h"
 #include "chrome/browser/autocomplete/aim_eligibility_service_factory.h"
+#include "chrome/browser/autocomplete/autocomplete_classifier_factory.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/command_updater.h"
 #include "chrome/browser/page_info/merchant_trust_service_factory.h"
@@ -101,6 +102,7 @@
 #include "components/contextual_search/input_state_model.h"
 #include "components/favicon/content/content_favicon_driver.h"
 #include "components/lens/lens_features.h"
+#include "components/omnibox/browser/autocomplete_classifier.h"
 #include "components/omnibox/browser/location_bar_model.h"
 #include "components/omnibox/browser/omnibox_client.h"
 #include "components/omnibox/browser/omnibox_field_trial.h"
@@ -116,6 +118,7 @@
 #include "components/search_engines/template_url_service.h"
 #include "components/security_state/content/security_state_tab_helper.h"
 #include "components/security_state/core/security_state.h"
+#include "content/public/browser/child_process_security_policy.h"
 #include "content/public/browser/navigation_entry.h"
 #include "content/public/browser/render_widget_host_view.h"
 #include "content/public/browser/web_contents.h"
@@ -2173,6 +2176,15 @@
     return;
   }
 
+  AutocompleteMatch match;
+  AutocompleteClassifierFactory::GetForProfile(GetProfile())
+      ->Classify(text, false, false, metrics::OmniboxEventProto::BLANK, &match,
+                 nullptr);
+  if (!content::ChildProcessSecurityPolicy::GetInstance()->IsWebSafeScheme(
+          std::string(match.destination_url.scheme()))) {
+    return;
+  }
+
   GetOmniboxController()->edit_model()->PasteAndGo(text, event_timestamp);
 }
 
diff --git a/chrome/browser/ui/browser_commands_browsertest.cc b/chrome/browser/ui/browser_commands_browsertest.cc
index 404302e..4a97ed0 100644
--- a/chrome/browser/ui/browser_commands_browsertest.cc
+++ b/chrome/browser/ui/browser_commands_browsertest.cc
@@ -38,7 +38,10 @@
 #include "content/public/common/content_paths.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
+#include "content/public/test/test_navigation_observer.h"
 #include "net/dns/mock_host_resolver.h"
+#include "ui/base/clipboard/clipboard.h"
+#include "ui/base/clipboard/scoped_clipboard_writer.h"
 #include "ui/base/ui_base_features.h"
 
 namespace chrome {
@@ -791,4 +794,60 @@
   EXPECT_EQ(1, browser()->tab_strip_model()->count());
 }
 
+#if BUILDFLAG(IS_LINUX)
+// Tests that unsafe schemes are not allowed when opening new tabs from a
+// clipboard URL.
+IN_PROC_BROWSER_TEST_F(BrowserCommandsTest,
+                       NewTabFromClipboardURLBlocksUnsafeSchemes) {
+  if (!ui::Clipboard::IsSupportedClipboardBuffer(
+          ui::ClipboardBuffer::kSelection)) {
+    return;
+  }
+
+  TabStripModel* tab_strip_model = browser()->tab_strip_model();
+  int initial_tab_count = tab_strip_model->count();
+
+  // Try file:// URL. Note: ui::Clipboard::ReadText is asynchronous on Linux, so
+  // we must pump the runloop to allow the callback to run and be rejected.
+  {
+    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kSelection);
+    writer.WriteText(u"file:///etc/passwd");
+  }
+
+  chrome::NewTabFromClipboardURL(browser());
+  base::RunLoop().RunUntilIdle();
+  EXPECT_EQ(initial_tab_count, tab_strip_model->count());
+
+  // Try chrome:// URL.
+  {
+    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kSelection);
+    writer.WriteText(u"chrome://version");
+  }
+
+  chrome::NewTabFromClipboardURL(browser());
+  base::RunLoop().RunUntilIdle();
+  EXPECT_EQ(initial_tab_count, tab_strip_model->count());
+
+  // Try a safe URL (http). We explicitly observe both the new tab addition and
+  // the navigation completion instead of guessing runloop cycles.
+  GURL safe_url = https_server_.GetURL("a.test", "/title1.html");
+  {
+    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kSelection);
+    writer.WriteText(base::UTF8ToUTF16(safe_url.spec()));
+  }
+
+  ui_test_utils::TabAddedWaiter tab_waiter(browser());
+  content::TestNavigationObserver observer(safe_url);
+  observer.StartWatchingNewWebContents();
+
+  chrome::NewTabFromClipboardURL(browser());
+  tab_waiter.Wait();
+  observer.Wait();
+
+  EXPECT_EQ(initial_tab_count + 1, tab_strip_model->count());
+  EXPECT_EQ(safe_url,
+            tab_strip_model->GetActiveWebContents()->GetLastCommittedURL());
+}
+#endif
+
 }  // namespace chrome
diff --git a/chrome/browser/ui/views/location_bar/location_bar_view_browsertest.cc b/chrome/browser/ui/views/location_bar/location_bar_view_browsertest.cc
index 38d81f2..6a0ecd60 100644
--- a/chrome/browser/ui/views/location_bar/location_bar_view_browsertest.cc
+++ b/chrome/browser/ui/views/location_bar/location_bar_view_browsertest.cc
@@ -754,3 +754,54 @@
   EXPECT_TRUE(regular_model_action->GetText().empty());
   EXPECT_FALSE(regular_model_action->GetImage().IsEmpty());
 }
+
+// Tests that unsafe schemes are not allowed to be opened from middle clicks.
+IN_PROC_BROWSER_TEST_F(LocationBarViewBrowserTest,
+                       MiddleClickPasteAndGoBlocksUnsafeSchemes) {
+  if (!ui::Clipboard::IsMiddleClickPasteEnabled() ||
+      !ui::Clipboard::IsSupportedClipboardBuffer(
+          ui::ClipboardBuffer::kSelection)) {
+    return;
+  }
+
+  ASSERT_TRUE(embedded_test_server()->Start());
+  GURL start_url = embedded_test_server()->GetURL("/title1.html");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), start_url));
+
+  LocationBarView* location_bar_view = GetLocationBarView();
+  LocationIconView* location_icon_view =
+      location_bar_view->location_icon_view();
+
+  // Try file:// URL. Note: ui::Clipboard::ReadText is asynchronous on Linux, so
+  // we must pump the runloop to allow the callback to run and be rejected.
+  {
+    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kSelection);
+    writer.WriteText(u"file:///etc/passwd");
+  }
+
+  ui::MouseEvent middle_click_event(ui::EventType::kMousePressed, gfx::Point(),
+                                    gfx::Point(), base::TimeTicks::Now(),
+                                    ui::EF_MIDDLE_MOUSE_BUTTON,
+                                    ui::EF_MIDDLE_MOUSE_BUTTON);
+  location_icon_view->OnMousePressed(middle_click_event);
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_EQ(start_url, browser()
+                           ->tab_strip_model()
+                           ->GetActiveWebContents()
+                           ->GetLastCommittedURL());
+
+  // Try chrome:// URL.
+  {
+    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kSelection);
+    writer.WriteText(u"chrome://version");
+  }
+
+  location_icon_view->OnMousePressed(middle_click_event);
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_EQ(start_url, browser()
+                           ->tab_strip_model()
+                           ->GetActiveWebContents()
+                           ->GetLastCommittedURL());
+}
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/browser_commands_browsertest.cc b/chrome/browser/ui/browser_commands_browsertest.cc
index 404302e..4a97ed0 100644
--- a/chrome/browser/ui/browser_commands_browsertest.cc
+++ b/chrome/browser/ui/browser_commands_browsertest.cc
@@ -38,7 +38,10 @@
 #include "content/public/common/content_paths.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
+#include "content/public/test/test_navigation_observer.h"
 #include "net/dns/mock_host_resolver.h"
+#include "ui/base/clipboard/clipboard.h"
+#include "ui/base/clipboard/scoped_clipboard_writer.h"
 #include "ui/base/ui_base_features.h"
 
 namespace chrome {
@@ -791,4 +794,60 @@
   EXPECT_EQ(1, browser()->tab_strip_model()->count());
 }
 
+#if BUILDFLAG(IS_LINUX)
+// Tests that unsafe schemes are not allowed when opening new tabs from a
+// clipboard URL.
+IN_PROC_BROWSER_TEST_F(BrowserCommandsTest,
+                       NewTabFromClipboardURLBlocksUnsafeSchemes) {
+  if (!ui::Clipboard::IsSupportedClipboardBuffer(
+          ui::ClipboardBuffer::kSelection)) {
+    return;
+  }
+
+  TabStripModel* tab_strip_model = browser()->tab_strip_model();
+  int initial_tab_count = tab_strip_model->count();
+
+  // Try file:// URL. Note: ui::Clipboard::ReadText is asynchronous on Linux, so
+  // we must pump the runloop to allow the callback to run and be rejected.
+  {
+    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kSelection);
+    writer.WriteText(u"file:///etc/passwd");
+  }
+
+  chrome::NewTabFromClipboardURL(browser());
+  base::RunLoop().RunUntilIdle();
+  EXPECT_EQ(initial_tab_count, tab_strip_model->count());
+
+  // Try chrome:// URL.
+  {
+    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kSelection);
+    writer.WriteText(u"chrome://version");
+  }
+
+  chrome::NewTabFromClipboardURL(browser());
+  base::RunLoop().RunUntilIdle();
+  EXPECT_EQ(initial_tab_count, tab_strip_model->count());
+
+  // Try a safe URL (http). We explicitly observe both the new tab addition and
+  // the navigation completion instead of guessing runloop cycles.
+  GURL safe_url = https_server_.GetURL("a.test", "/title1.html");
+  {
+    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kSelection);
+    writer.WriteText(base::UTF8ToUTF16(safe_url.spec()));
+  }
+
+  ui_test_utils::TabAddedWaiter tab_waiter(browser());
+  content::TestNavigationObserver observer(safe_url);
+  observer.StartWatchingNewWebContents();
+
+  chrome::NewTabFromClipboardURL(browser());
+  tab_waiter.Wait();
+  observer.Wait();
+
+  EXPECT_EQ(initial_tab_count + 1, tab_strip_model->count());
+  EXPECT_EQ(safe_url,
+            tab_strip_model->GetActiveWebContents()->GetLastCommittedURL());
+}
+#endif
+
 }  // namespace chrome
diff --git a/chrome/browser/ui/views/location_bar/location_bar_view_browsertest.cc b/chrome/browser/ui/views/location_bar/location_bar_view_browsertest.cc
index 38d81f2..6a0ecd60 100644
--- a/chrome/browser/ui/views/location_bar/location_bar_view_browsertest.cc
+++ b/chrome/browser/ui/views/location_bar/location_bar_view_browsertest.cc
@@ -754,3 +754,54 @@
   EXPECT_TRUE(regular_model_action->GetText().empty());
   EXPECT_FALSE(regular_model_action->GetImage().IsEmpty());
 }
+
+// Tests that unsafe schemes are not allowed to be opened from middle clicks.
+IN_PROC_BROWSER_TEST_F(LocationBarViewBrowserTest,
+                       MiddleClickPasteAndGoBlocksUnsafeSchemes) {
+  if (!ui::Clipboard::IsMiddleClickPasteEnabled() ||
+      !ui::Clipboard::IsSupportedClipboardBuffer(
+          ui::ClipboardBuffer::kSelection)) {
+    return;
+  }
+
+  ASSERT_TRUE(embedded_test_server()->Start());
+  GURL start_url = embedded_test_server()->GetURL("/title1.html");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), start_url));
+
+  LocationBarView* location_bar_view = GetLocationBarView();
+  LocationIconView* location_icon_view =
+      location_bar_view->location_icon_view();
+
+  // Try file:// URL. Note: ui::Clipboard::ReadText is asynchronous on Linux, so
+  // we must pump the runloop to allow the callback to run and be rejected.
+  {
+    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kSelection);
+    writer.WriteText(u"file:///etc/passwd");
+  }
+
+  ui::MouseEvent middle_click_event(ui::EventType::kMousePressed, gfx::Point(),
+                                    gfx::Point(), base::TimeTicks::Now(),
+                                    ui::EF_MIDDLE_MOUSE_BUTTON,
+                                    ui::EF_MIDDLE_MOUSE_BUTTON);
+  location_icon_view->OnMousePressed(middle_click_event);
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_EQ(start_url, browser()
+                           ->tab_strip_model()
+                           ->GetActiveWebContents()
+                           ->GetLastCommittedURL());
+
+  // Try chrome:// URL.
+  {
+    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kSelection);
+    writer.WriteText(u"chrome://version");
+  }
+
+  location_icon_view->OnMousePressed(middle_click_event);
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_EQ(start_url, browser()
+                           ->tab_strip_model()
+                           ->GetActiveWebContents()
+                           ->GetLastCommittedURL());
+}
Loading diff…

Original Bug Report

reported by vm...@google.com

Renderer-controlled browser navigation via middle-click on Linux omnibox icon or tab button

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 Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: On Linux, a web page can silently overwrite the X11 PRIMARY selection clipboard without a user gesture by selecting text during non-activating input events like mousemove. If a user middle-clicks the omnibox lock/tune icon or the New Tab button, the browser performs a browser-initiated navigation to the poisoned clipboard content. This bypasses restrictions, allowing a renderer to force navigation to sensitive schemes like file:// or chrome://.

Affected files:

  • chrome/browser/ui/views/location_bar/location_bar_view.cc
  • chrome/browser/ui/views/tabs/tab_strip.cc
  • chrome/browser/ui/views/location_bar/location_icon_view.cc
  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/renderer_host/render_widget_host_view_aura.cc
  • components/omnibox/browser/omnibox_text_util.cc
  • chrome/browser/ui/omnibox/omnibox_edit_model.cc
  • chrome/browser/ui/views/frame/horizontal_tab_strip_region_view.cc

Estimated timestamp from git blame: 2026-04-17

Vulnerability Overview

On Linux systems supporting the X11 PRIMARY selection clipboard, a web page can programmatically overwrite this clipboard buffer without requiring transient user activation (a user gesture). It achieves this by calling input.select() during a common, non-activating input event such as mousemove.

This clipboard content is then utilized by certain browser UI components—specifically the omnibox lock/tune icon and the New Tab (+) button—when they are middle-clicked. Because these UI components initiate a navigation using the clipboard content as if it were user-typed input, it allows a renderer to force a browser-initiated navigation to sensitive schemes such as file://, chrome://, and devtools://, bypassing the security restrictions that normally block web-to-local navigations.

Technical Analysis

1. Renderer Poisoning of PRIMARY Selection

In Blink, programmatic selection changes usually require an input event to be synced to the browser. While a simple setInterval will fail the HandlingInputEvent() check in RenderFrameImpl::DidChangeSelection (content/renderer/render_frame_impl.cc:4377), an attacker can bypass this by calling input.select() inside a mousemove event handler. Because mousemove is an input event, the check passes, and RenderFrameImpl::SetSelectedText triggers the mojom::LocalFrameHost::TextSelectionChanged IPC.

2. Browser-Side Silent Clipboard Write

The browser process receives this IPC in RenderFrameHostImpl::TextSelectionChanged. It forwards the selection to RenderWidgetHostViewAura::OnTextSelectionChanged (content/browser/renderer_host/render_widget_host_view_aura.cc:3372). On Linux, this method directly writes the selected text to the system’s PRIMARY selection (ui::ClipboardBuffer::kSelection) via ui::ScopedClipboardWriter. Crucially, this path does not perform any transient user activation check, allowing the background script to silently overwrite the middle-click clipboard every time the mouse moves.

3. Vulnerable Sinks

The attacker’s payload (e.g., a restricted URL) now sits in the PRIMARY selection. If the user intends to middle-click-paste their own text or simply misses a click, they may hit one of two vulnerable sinks:

  • Location Icon Middle-Click: Middle-clicking the tune/lock icon triggers LocationBarView::OnLocationIconPressed (chrome/browser/ui/views/location_bar/location_bar_view.cc:2217). This reads the PRIMARY selection and calls PasteAndGo. While SanitizeTextForPaste removes javascript: URLs, it does not block other sensitive schemes.
  • New Tab Button Middle-Click: Middle-clicking the New Tab (+) button triggers TabStrip::NewTabButtonPressed and calls chrome::NewTabFromClipboardURL (chrome/browser/ui/browser_commands.cc:1184).

4. Navigation Bypass

Both sinks pass the attacker-controlled text to the AutocompleteClassifier. Schemes like chrome:// and file:// are recognized as valid. The browser then constructs a NavigateParams object to initiate the navigation. Because the navigation originates from the browser UI, the is_renderer_initiated flag defaults to false. The navigation subsystem treats this as a trusted, browser-initiated action, successfully bypassing the security checks that normally prevent renderers from navigating to restricted URLs.

Potential Reproduction Steps

Note: Our tooling agent does not have the ability to run code, but the following steps are suggested based on code analysis.

  1. On a Linux system (X11 or Wayland with primary selection), host a page with the following script:
    let input = document.createElement('input');
    input.style.position = 'absolute';
    input.style.left = '-9999px';
    document.body.appendChild(input);
    
    // Hijack the primary selection on mouse movement to bypass HandlingInputEvent() checks
    document.addEventListener('mousemove', () => {
      input.value = 'file:///etc/passwd'; // Or chrome://crash, chrome://settings
      input.select();
    });
    
  2. Open the page in Chrome and move the mouse over the viewport.
  3. Middle-click the tune/lock icon in the omnibox.
  4. Observe that the current tab navigates to file:///etc/passwd.
  5. Alternatively, middle-click the + (New Tab) button and observe the same URL opening in a new tab.

Suggested Fix

To address this issue, writing to the PRIMARY clipboard buffer (ui::ClipboardBuffer::kSelection) via the TextSelectionChanged IPC should be restricted.

  1. Browser-Side Enforcement: RenderWidgetHostViewAura::OnTextSelectionChanged should verify that the frame currently has transient user activation before writing to the clipboard.
  2. Renderer-Side Enforcement: Alternatively, the renderer (RenderFrameImpl::DidChangeSelection or SetSelectedText) should require transient user activation (not just HandlingInputEvent(), which is true for non-activating events like mousemove) before sending the TextSelectionChanged IPC to update the global clipboard.

Evaluated with Chrome root at commit: a1e33f5848218e21d4a16ae2c1bc94e815c30c7f


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