Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Link Preview
DescriptionInappropriate implementation in Link Preview
ComponentLink Preview
Bug ClassLogic Error
Tracker497336872
Fix commit831a9ebe6002 (chromium/src) +124/-3396
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • chrome/app/chrome_command_ids.h
  • chrome/app/generated_resources.grd
  • chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW.png.sha1
  • chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTCLICK.png.sha1
  • chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTHOVER.png.sha1
  • chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_LONGPRESS.png.sha1
  • chrome/browser/BUILD.gn
  • chrome/browser/about_flags.cc
From 831a9ebe6002555d0572d611e0cf98ce9df5d731 Mon Sep 17 00:00:00 2001
From: Takashi Toyoshima <[email protected]>
Date: Tue, 07 Apr 2026 22:30:47 -0700
Subject: [PATCH] LinkPreview: Remove Desktop Link Preview feature

This CL removes the experimental Desktop Link Preview feature from the
desktop codebase.

All associated code including feature flags, triggerer implementations,
browser-side managers, and relevant tests are deleted or modified. This
includes cleanup of dead code and policy enums identified during the
removal process.

NO_IFTTT=pre-existing incomplete format

Bug: 497336872
Change-Id: I9eefc80a0fde3270be5790abbaf38ffddb2676c4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7727990
Reviewed-by: Foromo Daniel Soromou <[email protected]>
Reviewed-by: Rakina Zata Amni <[email protected]>
Reviewed-by: Kenichi Ishibashi <[email protected]>
Reviewed-by: Kentaro Hara <[email protected]>
Commit-Queue: Takashi Toyoshima <[email protected]>
Reviewed-by: Greg Thompson <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1611235}
---

diff --git a/chrome/app/chrome_command_ids.h b/chrome/app/chrome_command_ids.h
index 8c34db5..d748794 100644
--- a/chrome/app/chrome_command_ids.h
+++ b/chrome/app/chrome_command_ids.h
@@ -366,7 +366,6 @@
 #define IDC_CONTENT_CONTEXT_COPYLINKTEXT 50107
 #define IDC_CONTENT_CONTEXT_OPENLINKINPROFILE 50108
 #define IDC_CONTENT_CONTEXT_OPENLINKBOOKMARKAPP 50109
-#define IDC_CONTENT_CONTEXT_OPENLINKPREVIEW 50110
 #define IDC_CONTENT_CONTEXT_OPENLINKSPLITVIEW 50111
 // Image items.
 #define IDC_CONTENT_CONTEXT_SAVEIMAGEAS 50120
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 6c22014..91483aa 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -694,31 +694,7 @@
           <message name="IDS_CONTENT_CONTEXT_OPENLINKWEBAPP_NEWTAB" desc="The name of the command to open a link in a new tab of the same app the user is currently in">
             Open link in new <ph name="APP">$1<ex>Gmail App</ex></ph> tab
           </message>
-          <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW" desc="The name of the Preview link command in the content area context menu">
-            Preview link
-          </message>
-          <if expr="is_macosx">
-            <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTCLICK" desc="The minor text of the Preview link command in the content area context menu, alt+click trigger">
-              ⌥+Click
-            </message>
-            <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTHOVER" desc="The minor text of the Preview link command in the content area context menu, alt+hover trigger">
-              ⌥+Hover
-            </message>
-            <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_LONGPRESS" desc="The minor text of the Preview link command in the content area context menu, long press trigger">
-              Long press
-            </message>
-          </if>
-          <if expr="not is_macosx">
-            <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTCLICK" desc="The minor text of the Preview link command in the content area context menu, alt+click trigger">
-              Alt+Click
-            </message>
-            <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTHOVER" desc="The minor text of the Preview link command in the content area context menu, alt+hover trigger">
-              Alt+Hover
-            </message>
-            <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_LONGPRESS" desc="The minor text of the Preview link command in the content area context menu, long press trigger">
-              Long press
-            </message>
-          </if>
+
 
           <message name="IDS_CONTENT_CONTEXT_SAVELINKAS" desc="The name of the Save Link As command in the content area context menu">
             Save lin&amp;k as...
@@ -1019,31 +995,7 @@
           <message name="IDS_CONTENT_CONTEXT_OPENLINKWEBAPP_NEWTAB" desc="In Title Case: The name of the command to open a link in a new tab of the same app the user is currently in">
             Open Link in new <ph name="APP">$1<ex>Gmail App</ex></ph> Tab
           </message>
-          <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW" desc="In Title Case: The name of the Preview link command in the content area context menu">
-            Preview Link
-          </message>
-          <if expr="is_macosx">
-            <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTCLICK" desc="In Title Case: The minor text of the Preview link command in the content area context menu, alt+click trigger">
-              ⌥+Click
-            </message>
-            <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTHOVER" desc="In Title Case: The minor text of the Preview link command in the content area context menu, alt+hover trigger">
-              ⌥+Hover
-            </message>
-            <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_LONGPRESS" desc="In Title Case: The minor text of the Preview link command in the content area context menu, long press trigger">
-              Long Press
-            </message>
-          </if>
-          <if expr="not is_macosx">
-            <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTCLICK" desc="In Title Case: The minor text of the Preview link command in the content area context menu, alt+click trigger">
-              Alt+Click
-            </message>
-            <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTHOVER" desc="In Title Case: The minor text of the Preview link command in the content area context menu, alt+hover trigger">
-              Alt+Hover
-            </message>
-            <message name="IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_LONGPRESS" desc="In Title Case: The minor text of the Preview link command in the content area context menu, long press trigger">
-              Long Press
-            </message>
-          </if>
+
 
           <message name="IDS_CONTENT_CONTEXT_SAVELINKAS" desc="In Title Case: The name of the Save Link As command in the content area context menu">
             Save Lin&amp;k As...
diff --git a/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW.png.sha1 b/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW.png.sha1
deleted file mode 100644
index 6a9be622..0000000
--- a/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-9540a02a6bba4bf6f21a525882a57d061564b270
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTCLICK.png.sha1 b/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTCLICK.png.sha1
deleted file mode 100644
index ed6497720..0000000
--- a/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTCLICK.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-e1afcbf6951bc7d998ef2570308e6fa552653a63
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTHOVER.png.sha1 b/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTHOVER.png.sha1
deleted file mode 100644
index 5333ea3..0000000
--- a/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_ALTHOVER.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-ac54ac00066f396ee8056297a570d283a6400877
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_LONGPRESS.png.sha1 b/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_LONGPRESS.png.sha1
deleted file mode 100644
index 790015f..0000000
--- a/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_OPENLINKPREVIEW_TRIGGER_LONGPRESS.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-f5a25ba31c19615e4b366d34fbb8e3a4597b5c8e
\ No newline at end of file
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 48b1d86..85541e7a0 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -3504,14 +3504,6 @@
       "policy/webhid_device_policy_handler.h",
       "preloading/prefetch/zero_suggest_prefetch/zero_suggest_prefetch_tab_helper.cc",
       "preloading/prefetch/zero_suggest_prefetch/zero_suggest_prefetch_tab_helper.h",
-      "preloading/preview/preview_manager.cc",
-      "preloading/preview/preview_manager.h",
-      "preloading/preview/preview_navigation_throttle.cc",
-      "preloading/preview/preview_navigation_throttle.h",
-      "preloading/preview/preview_tab.cc",
-      "preloading/preview/preview_tab.h",
-      "preloading/preview/preview_zoom_controller.cc",
-      "preloading/preview/preview_zoom_controller.h",
       "process_singleton_startup_lock.cc",
       "process_singleton_startup_lock.h",
       "profiles/delete_profile_helper.cc",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index afbd6d1..99558c0 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -3293,20 +3293,6 @@
          kReplaceSyncPromosWithSigninPromosNewSigninFeatures}};
 #endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
 
-#if !BUILDFLAG(IS_ANDROID)
-const FeatureEntry::FeatureParam kLinkPreviewTriggerTypeAltClick[] = {
-    {"trigger_type", "alt_click"}};
-const FeatureEntry::FeatureParam kLinkPreviewTriggerTypeAltHover[] = {
-    {"trigger_type", "alt_hover"}};
-const FeatureEntry::FeatureParam kLinkPreviewTriggerTypeLongPress[] = {
-    {"trigger_type", "long_press"}};
-
-const FeatureEntry::FeatureVariation kLinkPreviewTriggerTypeVariations[] = {
-    {"Alt + Click", kLinkPreviewTriggerTypeAltClick, nullptr},
-    {"Alt + Hover", kLinkPreviewTriggerTypeAltHover, nullptr},
-    {"Long Press", kLinkPreviewTriggerTypeLongPress, nullptr}};
-#endif  // !BUILDFLAG(IS_ANDROID)
-
 const FeatureEntry::FeatureParam kGroupSuggestionEnableRecentlyOpenedOnly[] = {
     {"group_suggestion_enable_recently_opened", "true"},
     {"group_suggestion_enable_switch_between", "false"},
@@ -9878,14 +9864,6 @@
      FEATURE_VALUE_TYPE(blink::features::kLockedMode)},
 #endif  // BUILDFLAG_(IS_CHROMEOS)
 
-#if !BUILDFLAG(IS_ANDROID)
-    {"link-preview", flag_descriptions::kLinkPreviewName,
-     flag_descriptions::kLinkPreviewDescription, kOsDesktop,
-     FEATURE_WITH_PARAMS_VALUE_TYPE(blink::features::kLinkPreview,
-                                    kLinkPreviewTriggerTypeVariations,
-                                    "LinkPreview")},
-#endif  // !BUILDFLAG_(IS_ANDROID)
-
     {"send-tab-to-self-enhanced-handoff",
      flag_descriptions::kSendTabToSelfEnhancedHandoffName,
      flag_descriptions::kSendTabToSelfEnhancedHandoffDescription, kOsAll,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/download/download_browsertest.cc b/chrome/browser/download/download_browsertest.cc
index 64d03108..a92ff1a 100644
--- a/chrome/browser/download/download_browsertest.cc
+++ b/chrome/browser/download/download_browsertest.cc
@@ -577,8 +577,6 @@
       public ::testing::WithParamInterface<network::mojom::ReferrerPolicy> {
  public:
   DownloadReferrerPolicyTest() {
-    // Link Preview hides alt+click. Disables it not to do so.
-    feature_list_.InitAndDisableFeature(blink::features::kLinkPreview);
   }
 
   void SetUpOnMainThread() override {
diff --git a/chrome/browser/page_load_metrics/page_load_metrics_browsertest.cc b/chrome/browser/page_load_metrics/page_load_metrics_browsertest.cc
index b82ec46d..1c43c23 100644
--- a/chrome/browser/page_load_metrics/page_load_metrics_browsertest.cc
+++ b/chrome/browser/page_load_metrics/page_load_metrics_browsertest.cc
@@ -38,7 +38,6 @@
 #include "chrome/browser/prefs/session_startup_pref.h"
 #include "chrome/browser/preloading/prefetch/no_state_prefetch/no_state_prefetch_manager_factory.h"
 #include "chrome/browser/preloading/prefetch/no_state_prefetch/no_state_prefetch_test_utils.h"
-#include "chrome/browser/preloading/preview/preview_test_util.h"
 #include "chrome/browser/preloading/scoped_prewarm_feature_list.h"
 #include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h"
 #include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h"
@@ -3921,25 +3920,6 @@
       internal::kBackgroundHistogramFirstContentfulPaint, 0);
 }
 
-#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_MAC)
-// The LinkPreview feature is implemented only on desktops, and window
-// implementation assumes the Aura for now.
-// TODO(crbug.com/305004651): Implement the feature for other platforms and
-// enable the following tests on the remaining platforms.
-class PageLoadMetricsPreviewBrowserTest : public PageLoadMetricsBrowserTest {
- public:
-  PageLoadMetricsPreviewBrowserTest() {
-    helper_ = std::make_unique<test::PreviewTestHelper>(
-        base::BindRepeating(&PageLoadMetricsPreviewBrowserTest::web_contents,
-                            base::Unretained(this)));
-  }
-
- protected:
-  std::unique_ptr<test::PreviewTestHelper> helper_;
-};
-
-#endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_MAC)
-
 class PageLoadMetricsBrowserTestTerminatedPage
     : public PageLoadMetricsBrowserTest {
  protected:
diff --git a/chrome/browser/preloading/preview/preview_browsertest.cc b/chrome/browser/preloading/preview/preview_browsertest.cc
deleted file mode 100644
index 2299b6c..0000000
--- a/chrome/browser/preloading/preview/preview_browsertest.cc
+++ /dev/null
@@ -1,431 +0,0 @@
-// Copyright 2023 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "base/barrier_closure.h"
-#include "base/files/file_path.h"
-#include "base/functional/bind.h"
-#include "base/path_service.h"
-#include "base/test/metrics/histogram_tester.h"
-#include "chrome/browser/chrome_content_browser_client.h"
-#include "chrome/browser/preloading/preview/preview_manager.h"
-#include "chrome/browser/preloading/preview/preview_test_util.h"
-#include "chrome/browser/preloading/preview/preview_zoom_controller.h"
-#include "chrome/browser/ui/browser.h"
-#include "chrome/browser/ui/tabs/tab_strip_model.h"
-#include "chrome/common/chrome_paths.h"
-#include "chrome/test/base/chrome_test_utils.h"
-#include "chrome/test/base/in_process_browser_test.h"
-#include "chrome/test/base/ui_test_utils.h"
-#include "content/public/browser/render_frame_host.h"
-#include "content/public/common/content_client.h"
-#include "content/public/test/browser_test.h"
-#include "content/public/test/browser_test_utils.h"
-#include "content/public/test/mojo_capability_control_test_util.h"
-#include "mojo/public/cpp/bindings/remote.h"
-#include "mojo/public/cpp/bindings/remote_set.h"
-#include "mojo/public/cpp/test_support/test_utils.h"
-#include "net/dns/mock_host_resolver.h"
-#include "net/test/embedded_test_server/embedded_test_server.h"
-#include "services/service_manager/public/cpp/interface_provider.h"
-#include "testing/gtest/include/gtest/gtest.h"
-#include "third_party/blink/public/common/features.h"
-#include "third_party/blink/public/common/navigation/preloading_headers.h"
-#include "third_party/blink/public/mojom/speculation_rules/speculation_rules.mojom.h"
-
-class PreviewBrowserTest : public InProcessBrowserTest {
- public:
-  PreviewBrowserTest()
-      : helper_(std::make_unique<test::PreviewTestHelper>(
-            base::BindRepeating(&PreviewBrowserTest::web_contents,
-                                base::Unretained(this)))) {}
-
-  void SetUp() override { InProcessBrowserTest::SetUp(); }
-
-  void SetUpOnMainThread() override {
-    https_server_ = std::make_unique<net::EmbeddedTestServer>(
-        net::EmbeddedTestServer::TYPE_HTTPS);
-
-    embedded_test_server()->RegisterRequestMonitor(base::BindRepeating(
-        &PreviewBrowserTest::MonitorResourceRequest, base::Unretained(this)));
-    https_server_->RegisterRequestMonitor(base::BindRepeating(
-        &PreviewBrowserTest::MonitorResourceRequest, base::Unretained(this)));
-    histogram_tester_ = std::make_unique<base::HistogramTester>();
-
-    host_resolver()->AddRule("*", "127.0.0.1");
-
-    embedded_test_server()->AddDefaultHandlers(GetChromeTestDataDir());
-    ASSERT_TRUE(embedded_test_server()->Start());
-
-    https_server_->SetSSLConfig(
-        net::test_server::EmbeddedTestServer::CERT_TEST_NAMES);
-    https_server_->AddDefaultHandlers(GetChromeTestDataDir());
-    ASSERT_TRUE(https_server_->Start());
-  }
-
-  void TearDownOnMainThread() override {
-    ASSERT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
-    ASSERT_TRUE(https_server_->ShutdownAndWaitUntilComplete());
-  }
-
-  GURL GetURL(const std::string& path) {
-    return https_server_->GetURL("a.test", path);
-  }
-
-  net::test_server::HttpRequest::HeaderMap GetObservedRequestHeadersFor(
-      const GURL& url) {
-    base::AutoLock auto_lock(lock_);
-    std::string path = url.PathForRequest();
-    return request_headers_by_path_[path];
-  }
-
-  net::EmbeddedTestServer& https_server() { return *https_server_.get(); }
-
-  content::WebContents* web_contents() {
-    return chrome_test_utils::GetActiveWebContents(this);
-  }
-
-  test::PreviewTestHelper& helper() { return *helper_.get(); }
-  base::HistogramTester& histogram_tester() { return *histogram_tester_.get(); }
-
- private:
-  void MonitorResourceRequest(const net::test_server::HttpRequest& request) {
-    ASSERT_FALSE(
-        content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
-    base::AutoLock auto_lock(lock_);
-    request_headers_by_path_.emplace(request.GetURL().PathForRequest(),
-                                     request.headers);
-  }
-
-  std::unique_ptr<net::EmbeddedTestServer> https_server_;
-
-  base::Lock lock_;
-  std::map<std::string, net::test_server::HttpRequest::HeaderMap>
-      request_headers_by_path_ GUARDED_BY(lock_);
-
-  std::unique_ptr<test::PreviewTestHelper> helper_;
-  std::unique_ptr<base::HistogramTester> histogram_tester_;
-};
-
-IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, SecPurposeHeader) {
-  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetURL("/title1.html")));
-
-  GURL preview_url = GetURL("/title2.html");
-  helper().InitiatePreview(preview_url);
-  helper().WaitUntilLoadFinished();
-
-  net::test_server::HttpRequest::HeaderMap headers =
-      GetObservedRequestHeadersFor(preview_url);
-  auto it = headers.find(blink::kSecPurposeHeaderName);
-  ASSERT_NE(it, headers.end());
-  EXPECT_EQ(it->second, blink::kSecPurposePrefetchPrerenderPreviewHeaderValue);
-}
-
-IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, CancelWhenPrimaryPageChanged) {
-  TabStripModel* tab_strip_model = browser()->tab_strip_model();
-
-  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetURL("/title1.html")));
-
-  helper().InitiatePreview(GetURL("/title2.html"));
-  helper().WaitUntilLoadFinished();
-
-  base::WeakPtr<content::WebContents> preview_web_contents =
-      helper().GetWebContentsForPreviewTab();
-  ASSERT_TRUE(preview_web_contents);
-
-  EXPECT_EQ(1, tab_strip_model->count());
-  EXPECT_EQ(true,
-            content::EvalJs(preview_web_contents, "document.prerendering"));
-
-  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetURL("/title1.html")));
-
-  ASSERT_FALSE(preview_web_contents);
-}
-
-IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, PromoteToNewTab) {
-  TabStripModel* tab_strip_model = browser()->tab_strip_model();
-
-  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetURL("/title1.html")));
-
-  helper().InitiatePreview(GetURL("/title2.html"));
-  helper().WaitUntilLoadFinished();
-
-  base::WeakPtr<content::WebContents> preview_web_contents =
-      helper().GetWebContentsForPreviewTab();
-  ASSERT_TRUE(preview_web_contents);
-
-  EXPECT_EQ(1, tab_strip_model->count());
-  EXPECT_EQ(true,
-            content::EvalJs(preview_web_contents, "document.prerendering"));
-
-  helper().PromoteToNewTab();
-
-  EXPECT_EQ(2, tab_strip_model->count());
-  EXPECT_EQ(false,
-            content::EvalJs(preview_web_contents, "document.prerendering"));
-}
-
-IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, TrivialSessionHistory) {
-  const std::string title1_path = "/title1.html";
-  const GURL title1_url = GetURL(title1_path);
-  const GURL title2_url = GetURL("/title2.html");
-  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), title1_url));
-
-  helper().InitiatePreview(title2_url);
-  helper().WaitUntilLoadFinished();
-
-  base::WeakPtr<content::WebContents> preview_web_contents =
-      helper().GetWebContentsForPreviewTab();
-  ASSERT_TRUE(preview_web_contents);
-
-  EXPECT_EQ(title2_url, preview_web_contents->GetLastCommittedURL());
-  EXPECT_EQ(1, preview_web_contents->GetController().GetEntryCount());
-  EXPECT_EQ(
-      1, EvalJs(preview_web_contents->GetPrimaryMainFrame(), "history.length"));
-
-  ASSERT_EQ(title1_path, EvalJs(preview_web_contents->GetPrimaryMainFrame(),
-                                "location = '/title1.html';"));
-  helper().WaitUntilLoadFinished();
-
-  EXPECT_EQ(title1_url, preview_web_contents->GetLastCommittedURL());
-  EXPECT_EQ(1, preview_web_contents->GetController().GetEntryCount());
-  EXPECT_EQ(
-      1, EvalJs(preview_web_contents->GetPrimaryMainFrame(), "history.length"));
-}
-
-class MojoCapabilityControlTestContentBrowserClient
-    : public ChromeContentBrowserClient,
-      public content::test::MojoCapabilityControlTestHelper {
- public:
-  MojoCapabilityControlTestContentBrowserClient() {
-    previous_client_ = content::SetBrowserClientForTesting(this);
-  }
-  ~MojoCapabilityControlTestContentBrowserClient() override {
-    content::SetBrowserClientForTesting(previous_client_);
-  }
-  MojoCapabilityControlTestContentBrowserClient(
-      const MojoCapabilityControlTestContentBrowserClient&) = delete;
-  MojoCapabilityControlTestContentBrowserClient& operator=(
-      const MojoCapabilityControlTestContentBrowserClient&) = delete;
-
- private:
-  // ChromeContentBrowserClient implementation.
-  void RegisterBrowserInterfaceBindersForFrame(
-      content::RenderFrameHost* render_frame_host,
-      mojo::BinderMapWithContext<content::RenderFrameHost*>* map) override {
-    if (previous_client_) {
-      previous_client_->RegisterBrowserInterfaceBindersForFrame(
-          render_frame_host, map);
-    }
-    RegisterTestBrowserInterfaceBindersForFrame(render_frame_host, map);
-  }
-  void RegisterMojoBinderPoliciesForPreview(
-      content::MojoBinderPolicyMap& policy_map) override {
-    RegisterTestMojoBinderPolicies(policy_map);
-  }
-
-  raw_ptr<content::ContentBrowserClient> previous_client_;
-};
-
-IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, MojoCapabilityControl) {
-  MojoCapabilityControlTestContentBrowserClient test_browser_client;
-
-  const GURL kInitialUrl = GetURL("/title1.html");
-  const GURL kPreviewUrl = GetURL("/page_with_iframe.html");
-
-  // Navigate to an initial page.
-  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), kInitialUrl));
-  ASSERT_EQ(web_contents()->GetLastCommittedURL(), kInitialUrl);
-
-  // Open the target page in preview mode.
-  helper().InitiatePreview(kPreviewUrl);
-  helper().WaitUntilLoadFinished();
-
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

LNA and Fetch Metadata bypass via LinkPreview navigation

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

Overview: When the experimental LinkPreview feature is enabled, a compromised renderer can trigger a browser-initiated navigation to an arbitrary HTTPS URL via the SpeculationHost Mojo interface. Because the navigation lacks initiator information, it bypasses Local Network Access (LNA) checks and sends a forged Sec-Fetch-Site header. This potentially allows unauthorized cross-site, credentialed GET requests to private network endpoints.

Affected files:

  • chrome/browser/preloading/preview/preview_tab.cc
  • ui/views/controls/webview/webview.cc
  • content/browser/preloading/speculation_rules/speculation_host_impl.cc
  • content/browser/renderer_host/navigation_controller_impl.cc
  • content/browser/renderer_host/navigation_policy_container_builder.cc
  • content/browser/renderer_host/navigation_request.cc
  • services/network/local_network_access_checker.cc
  • content/browser/loader/navigation_url_loader_impl.cc

Estimated timestamp from git blame: 2025-11-05

Description

When the experimental LinkPreview feature (blink::features::kLinkPreview) is enabled, a compromised renderer can initiate a preview of an arbitrary HTTP/HTTPS URL by sending a blink::mojom::SpeculationHost::InitiatePreview Mojo IPC message.

The browser handles this request in content::SpeculationHostImpl::InitiatePreview, which validates the scheme but fails to pass the initiator’s origin or frame token down to the UI layer. The navigation is eventually triggered in PreviewTab via views::WebView::LoadInitialURL. This function constructs a default content::NavigationController::LoadURLParams object where initiator_origin, initiator_frame_token, and is_renderer_initiated are left empty or false.

Because the navigation is treated as browser-initiated and lacks initiator context, it bypasses two critical security boundaries:

  1. Local Network Access (LNA) Bypass: During navigation request creation, NavigationPolicyContainerBuilder fails to find initiator policies (since frame_token is null). Consequently, NavigationRequest::BuildClientSecurityStateForNavigationFetch returns a null ClientSecurityState. When the network service checks LNA rules, LocalNetworkAccessChecker::CheckAddressSpace observes the missing state and defaults to allowing the request (Result::kAllowedMissingClientSecurityState), skipping private network protections.
  2. Fetch Metadata Bypass: NavigationURLLoaderImpl sets an empty request_initiator on the network request. The Network Service, assuming a user-typed navigation, computes the Fetch Metadata header Sec-Fetch-Site as none instead of cross-site. This allows an attacker to bypass server-side CSRF protections that rely on Fetch Metadata.

Note: The LinkPreview feature is currently disabled by default, and PreviewNavigationThrottle restricts navigations to HTTPS only. A compromised renderer is required to invoke the Mojo interface directly.

Potential Attacker Steps

Please note: These are suggested steps based on static code analysis. Our tooling agent does not currently have the ability to run code or provide a working proof-of-concept.

  1. An attacker compromises a renderer process (e.g., via a V8 memory corruption exploit) running on a public website.
  2. The compromised renderer constructs a blink::mojom::SpeculationHost::InitiatePreview Mojo message targeting a private network endpoint (e.g., https://192.168.1.1:8443/admin/reboot).
  3. The browser process receives the IPC and instantiates a PreviewTab without attaching the renderer’s initiator context.
  4. The browser performs a credentialed GET request to the target intranet URL.
  5. The request successfully reaches the private endpoint, bypassing LNA blocks and carrying a Sec-Fetch-Site: none header, potentially triggering a state-changing action if the endpoint is vulnerable to CSRF via GET.

Suggested Fix

  1. Modify PreviewTab and views::WebView::LoadInitialURL (or provide an alternative method) to accept the initiator’s url::Origin and blink::LocalFrameToken.
  2. When constructing the content::NavigationController::LoadURLParams in the UI layer, explicitly set initiator_origin, initiator_frame_token, and set is_renderer_initiated = true.
  3. Alternatively, SpeculationHostImpl::InitiatePreview could perform the LNA validation directly before passing the URL to the UI layer.

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