Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect reference resolution in Loader
DescriptionIncorrect reference resolution in Loader
ComponentLoader
Bug ClassLogic Error
Tracker502139081
Fix commitc943aa2ef9f1 (chromium/src) +195/-117
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
TEST
chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util_unittest.cc
modified
for
chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util_unittest.cc
modified

Files Changed

  • chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util.cc
  • chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util_unittest.cc
From c943aa2ef9f17f60658eaefcb736e89665d478dc Mon Sep 17 00:00:00 2001
From: Minoru Chikamune <chikamune@chromium.org>
Date: Wed, 15 Jul 2026 04:59:32 -0700
Subject: [PATCH] LCPP: Restrict LCPP database and navigation hints to HTTPS only

This CL restricts LCP Critical Path Predictor (LCPP) data collection,
LCPP-based subresource prewarming, and LCPP navigation time hints
strictly to HTTPS URLs. To support this change, LCPP unit tests and
browser tests are migrated to use HTTPS.

Fixed: 502139081
Change-Id: I589a45a7f3e63b7e46bd1fb006a499c187176c1d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8085005
Reviewed-by: Lingqi Chi <lingqi@chromium.org>
Reviewed-by: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Commit-Queue: Minoru Chikamune <chikamune@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1662520}
---

diff --git a/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util.cc b/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util.cc
index 3198fcca..6e30489b 100644
--- a/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util.cc
+++ b/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util.cc
@@ -1130,7 +1130,7 @@
 
 bool IsURLValidForLcpp(const GURL& url) {
   return url.is_valid() && !url.GetHost().empty() && !net::IsLocalhost(url) &&
-         url.SchemeIsHTTPOrHTTPS() &&
+         url.SchemeIs(url::kHttpsScheme) &&
          url.GetHost().size() <=
              ResourcePrefetchPredictorTables::kMaxStringLength;
 }
@@ -1147,7 +1147,7 @@
     GURL url = initiator_origin.GetURL();
     return !initiator_origin.opaque() && url.is_valid() &&
            !initiator_origin.host().empty() && !net::IsLocalhost(url) &&
-           url.SchemeIsHTTPOrHTTPS() &&
+           url.SchemeIs(url::kHttpsScheme) &&
            initiator_origin.host().size() <=
                ResourcePrefetchPredictorTables::kMaxStringLength;
   } else {
diff --git a/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util_unittest.cc b/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util_unittest.cc
index 95fb78f..043bf866 100644
--- a/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util_unittest.cc
+++ b/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util_unittest.cc
@@ -1224,16 +1224,19 @@
 TEST(LcppKeyTest, InvalidURLs) {
   const std::string invalid_urls[] = {
       // Invalid urls
-      "http://?k=v",
-      "http:://google.com",
-      "http://google.com:12three45",
+      "https://?k=v",
+      "https:://google.com",
+      "https://google.com:12three45",
       "://google.com",
       "path",
-      "",                  // Empty
-      "file://server:0",   // File
-      "ftp://server",      // Ftp
-      "http://localhost",  // Localhost
-      "http://127.0.0.1",  // Localhost
+      "",                   // Empty
+      "file://server:0",    // File
+      "ftp://server",       // Ftp
+      "http://a.test",      // Non-HTTPS scheme
+      "ws://a.test",        // Non-HTTPS scheme
+      "wss://a.test",       // Non-HTTPS scheme (we only support HTTPS)
+      "https://localhost",  // Localhost
+      "https://127.0.0.1",  // Localhost
       "https://example" +
           std::string(ResourcePrefetchPredictorTables::kMaxStringLength, 'a') +
           ".test/",  // Too long
@@ -1258,24 +1261,24 @@
   const std::string too_long_path =
       "/" + std::string(max_path_length + 1, 'c') + "/bar";
   const std::vector<std::pair<std::string, std::string>> url_keys = {
-      {"http://a.test", ""},
-      {"http://user:pass@a.test:99/foo;bar?q=a#ref", "/foo;bar"},
-      {"http://a.test/", ""},
-      {"http://a.test/foo.html", ""},
-      {"http://a.test/foo", "/foo"},
-      {"http://a.test/foo/", "/foo"},
-      {"http://a.test/foo/bar", "/foo"},
-      {"http://a.test/foo/bar/", "/foo"},
-      {"http://a.test/foo/bar/baz.com", "/foo"},
-      {"http://a.test/bar?q=c", "/bar"},
-      {"http://a.test/foo/bar?q=c", "/foo"},
-      {"http://a.test" + long_path, long_path},
-      {"http://a.test" + long_path + "/bar", long_path},
-      {"http://a.test" + long_path + "bar", ""},
+      {"https://a.test", ""},
+      {"https://user:pass@a.test:99/foo;bar?q=a#ref", "/foo;bar"},
+      {"https://a.test/", ""},
+      {"https://a.test/foo.html", ""},
+      {"https://a.test/foo", "/foo"},
+      {"https://a.test/foo/", "/foo"},
+      {"https://a.test/foo/bar", "/foo"},
+      {"https://a.test/foo/bar/", "/foo"},
+      {"https://a.test/foo/bar/baz.com", "/foo"},
+      {"https://a.test/bar?q=c", "/bar"},
+      {"https://a.test/foo/bar?q=c", "/foo"},
+      {"https://a.test" + long_path, long_path},
+      {"https://a.test" + long_path + "/bar", long_path},
+      {"https://a.test" + long_path + "bar", ""},
       // Too long path is ignored.
-      {"http://a.test" + too_long_path, ""},
+      {"https://a.test" + too_long_path, ""},
       // Invalid length path in subdirectory is also ignored.
-      {"http://a.test/bar" + too_long_path, "/bar"}};
+      {"https://a.test/bar" + too_long_path, "/bar"}};
 
   for (const auto& url_key : url_keys) {
     const GURL url(url_key.first);
@@ -1427,7 +1430,7 @@
       LearnElementLocator(GURL(url), "/#a", {});
       // Confirm 'url' was learned as 'key'.
       auto stat = lcpp_data_map_->GetLcppStat(/*initiator_origin=*/std::nullopt,
-                                              GURL("http://" + key));
+                                              GURL("https://" + key));
       EXPECT_TRUE(stat) << location.ToString() << url;
       LcppData expected;
       InitializeLcpElementLocatorBucket(expected, "/#a", ++frequency[key]);
@@ -1457,7 +1460,8 @@
   }
 
   static url::Origin CreateOrigin(const std::string& host_name) {
-    const url::Origin origin = url::Origin::Create(GURL("http://" + host_name));
+    const url::Origin origin =
+        url::Origin::Create(GURL("https://" + host_name));
     CHECK_EQ(origin.host(), host_name);
     return origin;
   }
@@ -1626,7 +1630,7 @@
   };
 
   for (int i = 0; i < 3; ++i) {
-    LearnElementLocator(GURL("http://a.test"), "/#a", {});
+    LearnElementLocator(GURL("https://a.test"), "/#a", {});
   }
   {
     LcppData data = CreateLcppData("a.test", 10);
@@ -1636,7 +1640,7 @@
   }
 
   for (int i = 0; i < 2; ++i) {
-    LearnElementLocator(GURL("http://a.test"), "/#b", {});
+    LearnElementLocator(GURL("https://a.test"), "/#b", {});
   }
   {
     LcppData data = CreateLcppData("a.test", 10);
@@ -1646,7 +1650,7 @@
     EXPECT_DOUBLE_EQ(5, SumOfElementLocatorFrequency(data));
   }
 
-  LearnElementLocator(GURL("http://a.test"), "/#c", {});
+  LearnElementLocator(GURL("https://a.test"), "/#c", {});
   {
     LcppData data = CreateLcppData("a.test", 10);
     InitializeLcpElementLocatorBucket(data, "/#a", 2.4);
@@ -1656,7 +1660,7 @@
     EXPECT_DOUBLE_EQ(5, SumOfElementLocatorFrequency(data));
   }
 
-  LearnElementLocator(GURL("http://a.test"), "/#d", {});
+  LearnElementLocator(GURL("https://a.test"), "/#d", {});
   {
     LcppData data = CreateLcppData("a.test", 10);
     InitializeLcpElementLocatorBucket(data, "/#a", 1.92);
@@ -1667,8 +1671,8 @@
   }
 
   for (int i = 0; i < 2; ++i) {
-    LearnElementLocator(GURL("http://a.test"), "/#c", {});
-    LearnElementLocator(GURL("http://a.test"), "/#d", {});
+    LearnElementLocator(GURL("https://a.test"), "/#c", {});
+    LearnElementLocator(GURL("https://a.test"), "/#d", {});
   }
   {
     LcppData data = CreateLcppData("a.test", 10);
@@ -1682,7 +1686,7 @@
   // Test that element locators and influencer scripts are independently learnt.
   for (int i = 0; i < 2; ++i) {
     LearnElementLocator(
-        GURL("http://a.test"), "",
+        GURL("https://a.test"), "",
         {GURL("https://a.test/script1.js"), GURL("https://a.test/script2.js")});
   }
   {
@@ -1702,7 +1706,7 @@
 
   for (int i = 0; i < 3; ++i) {
     LearnElementLocator(
-        GURL("http://a.test"), "",
+        GURL("https://a.test"), "",
         {GURL("https://a.test/script3.js"), GURL("https://a.test/script4.js")});
   }
   {
@@ -1731,7 +1735,7 @@
         data.lcpp_stat().fetched_font_url_stat());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util_unittest.cc b/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util_unittest.cc
index 95fb78f..043bf866 100644
--- a/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util_unittest.cc
+++ b/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util_unittest.cc
@@ -1224,16 +1224,19 @@
 TEST(LcppKeyTest, InvalidURLs) {
   const std::string invalid_urls[] = {
       // Invalid urls
-      "http://?k=v",
-      "http:://google.com",
-      "http://google.com:12three45",
+      "https://?k=v",
+      "https:://google.com",
+      "https://google.com:12three45",
       "://google.com",
       "path",
-      "",                  // Empty
-      "file://server:0",   // File
-      "ftp://server",      // Ftp
-      "http://localhost",  // Localhost
-      "http://127.0.0.1",  // Localhost
+      "",                   // Empty
+      "file://server:0",    // File
+      "ftp://server",       // Ftp
+      "http://a.test",      // Non-HTTPS scheme
+      "ws://a.test",        // Non-HTTPS scheme
+      "wss://a.test",       // Non-HTTPS scheme (we only support HTTPS)
+      "https://localhost",  // Localhost
+      "https://127.0.0.1",  // Localhost
       "https://example" +
           std::string(ResourcePrefetchPredictorTables::kMaxStringLength, 'a') +
           ".test/",  // Too long
@@ -1258,24 +1261,24 @@
   const std::string too_long_path =
       "/" + std::string(max_path_length + 1, 'c') + "/bar";
   const std::vector<std::pair<std::string, std::string>> url_keys = {
-      {"http://a.test", ""},
-      {"http://user:pass@a.test:99/foo;bar?q=a#ref", "/foo;bar"},
-      {"http://a.test/", ""},
-      {"http://a.test/foo.html", ""},
-      {"http://a.test/foo", "/foo"},
-      {"http://a.test/foo/", "/foo"},
-      {"http://a.test/foo/bar", "/foo"},
-      {"http://a.test/foo/bar/", "/foo"},
-      {"http://a.test/foo/bar/baz.com", "/foo"},
-      {"http://a.test/bar?q=c", "/bar"},
-      {"http://a.test/foo/bar?q=c", "/foo"},
-      {"http://a.test" + long_path, long_path},
-      {"http://a.test" + long_path + "/bar", long_path},
-      {"http://a.test" + long_path + "bar", ""},
+      {"https://a.test", ""},
+      {"https://user:pass@a.test:99/foo;bar?q=a#ref", "/foo;bar"},
+      {"https://a.test/", ""},
+      {"https://a.test/foo.html", ""},
+      {"https://a.test/foo", "/foo"},
+      {"https://a.test/foo/", "/foo"},
+      {"https://a.test/foo/bar", "/foo"},
+      {"https://a.test/foo/bar/", "/foo"},
+      {"https://a.test/foo/bar/baz.com", "/foo"},
+      {"https://a.test/bar?q=c", "/bar"},
+      {"https://a.test/foo/bar?q=c", "/foo"},
+      {"https://a.test" + long_path, long_path},
+      {"https://a.test" + long_path + "/bar", long_path},
+      {"https://a.test" + long_path + "bar", ""},
       // Too long path is ignored.
-      {"http://a.test" + too_long_path, ""},
+      {"https://a.test" + too_long_path, ""},
       // Invalid length path in subdirectory is also ignored.
-      {"http://a.test/bar" + too_long_path, "/bar"}};
+      {"https://a.test/bar" + too_long_path, "/bar"}};
 
   for (const auto& url_key : url_keys) {
     const GURL url(url_key.first);
@@ -1427,7 +1430,7 @@
       LearnElementLocator(GURL(url), "/#a", {});
       // Confirm 'url' was learned as 'key'.
       auto stat = lcpp_data_map_->GetLcppStat(/*initiator_origin=*/std::nullopt,
-                                              GURL("http://" + key));
+                                              GURL("https://" + key));
       EXPECT_TRUE(stat) << location.ToString() << url;
       LcppData expected;
       InitializeLcpElementLocatorBucket(expected, "/#a", ++frequency[key]);
@@ -1457,7 +1460,8 @@
   }
 
   static url::Origin CreateOrigin(const std::string& host_name) {
-    const url::Origin origin = url::Origin::Create(GURL("http://" + host_name));
+    const url::Origin origin =
+        url::Origin::Create(GURL("https://" + host_name));
     CHECK_EQ(origin.host(), host_name);
     return origin;
   }
@@ -1626,7 +1630,7 @@
   };
 
   for (int i = 0; i < 3; ++i) {
-    LearnElementLocator(GURL("http://a.test"), "/#a", {});
+    LearnElementLocator(GURL("https://a.test"), "/#a", {});
   }
   {
     LcppData data = CreateLcppData("a.test", 10);
@@ -1636,7 +1640,7 @@
   }
 
   for (int i = 0; i < 2; ++i) {
-    LearnElementLocator(GURL("http://a.test"), "/#b", {});
+    LearnElementLocator(GURL("https://a.test"), "/#b", {});
   }
   {
     LcppData data = CreateLcppData("a.test", 10);
@@ -1646,7 +1650,7 @@
     EXPECT_DOUBLE_EQ(5, SumOfElementLocatorFrequency(data));
   }
 
-  LearnElementLocator(GURL("http://a.test"), "/#c", {});
+  LearnElementLocator(GURL("https://a.test"), "/#c", {});
   {
     LcppData data = CreateLcppData("a.test", 10);
     InitializeLcpElementLocatorBucket(data, "/#a", 2.4);
@@ -1656,7 +1660,7 @@
     EXPECT_DOUBLE_EQ(5, SumOfElementLocatorFrequency(data));
   }
 
-  LearnElementLocator(GURL("http://a.test"), "/#d", {});
+  LearnElementLocator(GURL("https://a.test"), "/#d", {});
   {
     LcppData data = CreateLcppData("a.test", 10);
     InitializeLcpElementLocatorBucket(data, "/#a", 1.92);
@@ -1667,8 +1671,8 @@
   }
 
   for (int i = 0; i < 2; ++i) {
-    LearnElementLocator(GURL("http://a.test"), "/#c", {});
-    LearnElementLocator(GURL("http://a.test"), "/#d", {});
+    LearnElementLocator(GURL("https://a.test"), "/#c", {});
+    LearnElementLocator(GURL("https://a.test"), "/#d", {});
   }
   {
     LcppData data = CreateLcppData("a.test", 10);
@@ -1682,7 +1686,7 @@
   // Test that element locators and influencer scripts are independently learnt.
   for (int i = 0; i < 2; ++i) {
     LearnElementLocator(
-        GURL("http://a.test"), "",
+        GURL("https://a.test"), "",
         {GURL("https://a.test/script1.js"), GURL("https://a.test/script2.js")});
   }
   {
@@ -1702,7 +1706,7 @@
 
   for (int i = 0; i < 3; ++i) {
     LearnElementLocator(
-        GURL("http://a.test"), "",
+        GURL("https://a.test"), "",
         {GURL("https://a.test/script3.js"), GURL("https://a.test/script4.js")});
   }
   {
@@ -1731,7 +1735,7 @@
         data.lcpp_stat().fetched_font_url_stat());
   };
   for (int i = 0; i < 2; ++i) {
-    LearnFontUrls(GURL("http://example.test"),
+    LearnFontUrls(GURL("https://example.test"),
                   {
                       GURL("https://example.test/test.woff"),
                       GURL("https://example.test/test.ttf"),
@@ -1748,7 +1752,7 @@
     EXPECT_DOUBLE_EQ(4, SumOfFontUrlFrequency(data));
   }
   for (int i = 0; i < 3; ++i) {
-    LearnFontUrls(GURL("http://example.test"),
+    LearnFontUrls(GURL("https://example.test"),
                   {
                       GURL("https://example.org/test.otf"),
                       GURL("https://example.net/test.svg"),
@@ -1776,7 +1780,7 @@
       network::mojom::RequestDestination::kImage;
   const int32_t kImageValue = static_cast<int32_t>(kImage);
   const std::string kUrl = "example.test";
-  const GURL kGURL = GURL("http://" + kUrl);
+  const GURL kGURL = GURL("https://" + kUrl);
   const std::string kJpegA = "https://" + kUrl + "/a.jpeg";
   const std::string kJpegB = "https://" + kUrl + "/b.jpeg";
 
@@ -1864,7 +1868,7 @@
   }
 
   // Confirm that new learning process reset the corrupted data.
-  LearnElementLocator(GURL("http://a.test"), "/#a", {});
+  LearnElementLocator(GURL("https://a.test"), "/#a", {});
   {
     LcppData data = CreateLcppData("a.test", 10);
     InitializeLcpElementLocatorBucket(data, "/#a", 1);
@@ -1879,21 +1883,21 @@
   InitializeDB(config);
   EXPECT_TRUE(GetDataMap().empty());
 
-  const GURL url_a("http://a.test");
+  const GURL url_a("https://a.test");
   EXPECT_FALSE(GetLcppStat(url_a));
 
   LearnElementLocator(url_a, "/#a");
   EXPECT_TRUE(GetLcppStat(url_a));
 
-  const GURL url_b("http://b.test");
+  const GURL url_b("https://b.test");
   LearnElementLocator(url_b, "/#a");
-  const GURL url_c("http://c.test");
+  const GURL url_c("https://c.test");
   LearnElementLocator(url_c, "/#a");
   EXPECT_TRUE(GetLcppStat(url_a));
   EXPECT_TRUE(GetLcppStat(url_b));
   EXPECT_TRUE(GetLcppStat(url_c));
 
-  const GURL url_d("http://d.test");
+  const GURL url_d("https://d.test");
   LearnElementLocator(url_d, "/#a");
   EXPECT_TRUE(GetLcppStat(url_d));
   // Confirm first host is dropped.
@@ -1914,11 +1918,11 @@
   InitializeDB(config);
 
   const std::vector<std::pair<std::string, std::string>> url_keys = {
-      {"http://a.test", "a.test"},
-      {"http://a.test/", "a.test"},
-      {"http://a.test/foo", "a.test/foo"},
-      {"http://a.test/bar?q=c", "a.test/bar?q=c"},
-      {"http://user:pass@a.test:99/foo;bar?q=a#ref", "a.test/foo;bar?q=a#ref"},
+      {"https://a.test", "a.test"},
+      {"https://a.test/", "a.test"},
+      {"https://a.test/foo", "a.test/foo"},
+      {"https://a.test/bar?q=c", "a.test/bar?q=c"},
+      {"https://user:pass@a.test:99/foo;bar?q=a#ref", "a.test/foo;bar?q=a#ref"},
   };
 
   TestLearnLcppURL(url_keys);
@@ -1930,9 +1934,9 @@
   config.max_hosts_to_track_for_lcpp = 10u;
   InitializeDB(config);
 
-  const GURL url_a("http://a.test");
-  const GURL url_b("http://b.test");
-  const GURL url_c("http://c.test");
+  const GURL url_a("https://a.test");
+  const GURL url_b("https://b.test");
+  const GURL url_c("https://c.test");
 
   LearnElementLocator(url_a, "/#a");
   LearnElementLocator(url_b, "/#a");
@@ -2005,33 +2009,33 @@
   const std::string too_long_path =
       "/" + std::string(max_path_length + 1, 'c') + "/bar";
   const std::vector<std::pair<std::string, std::string>> url_keys = {
-      {"http://a.test", "a.test"},
-      {"http://user:pass@a.test:99/foo;bar?q=a#ref", "a.test/foo;bar"},
-      {"http://a.test/", "a.test"},
-      {"http://a.test/foo.html", "a.test"},
-      {"http://a.test/foo", "a.test/foo"},
-      {"http://a.test/foo/", "a.test/foo"},
-      {"http://a.test/foo/bar", "a.test/foo"},
-      {"http://a.test/foo/bar/", "a.test/foo"},
-      {"http://a.test/foo/bar/baz.com", "a.test/foo"},
-      {"http://a.test/bar?q=c", "a.test/bar"},
-      {"http://a.test/foo/bar?q=c", "a.test/foo"},
-      {"http://a.test" + long_path, "a.test" + long_path},
-      {"http://a.test" + long_path + "/bar", "a.test" + long_path},
-      {"http://a.test" + long_path + "bar", "a.test"},
-      {"http://" + long_host + "/bar", long_host + "/bar"},
+      {"https://a.test", "a.test"},
+      {"https://user:pass@a.test:99/foo;bar?q=a#ref", "a.test/foo;bar"},
+      {"https://a.test/", "a.test"},
+      {"https://a.test/foo.html", "a.test"},
+      {"https://a.test/foo", "a.test/foo"},
+      {"https://a.test/foo/", "a.test/foo"},
+      {"https://a.test/foo/bar", "a.test/foo"},
+      {"https://a.test/foo/bar/", "a.test/foo"},
+      {"https://a.test/foo/bar/baz.com", "a.test/foo"},
+      {"https://a.test/bar?q=c", "a.test/bar"},
+      {"https://a.test/foo/bar?q=c", "a.test/foo"},
+      {"https://a.test" + long_path, "a.test" + long_path},
+      {"https://a.test" + long_path + "/bar", "a.test" + long_path},
+      {"https://a.test" + long_path + "bar", "a.test"},
+      {"https://" + long_host + "/bar", long_host + "/bar"},
       // Both valid but if the concated key is too long, take only host.
-      {"http://" + long_host + long_path, long_host},
+      {"https://" + long_host + long_path, long_host},
       // Too long path is ignored.
-      {"http://a.test" + too_long_path, "a.test"},
+      {"https://a.test" + too_long_path, "a.test"},
       // Invalid length path in subdirectory is also ignored.
-      {"http://a.test/bar" + too_long_path, "a.test/bar"}};
+      {"https://a.test/bar" + too_long_path, "a.test/bar"}};
 
   TestLearnLcppURL(url_keys);
 }
 
 TEST_P(LcppMultipleKeyTest, ShouldNotLearnTooLongLocators) {
-  const GURL url("http://a.test/foo1");
+  const GURL url("https://a.test/foo1");
   LearnElementLocator(url, "/#lcp");
... (truncated)
Loading diff…

Original Bug Report

reported by vm...@google.com

Persistent CSRF via LCPP database scheme confusion in subresource prefetching

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.

Overview: The LCP Critical Path Predictor (LCPP) uses a host-only key for its database, ignoring URL schemes. This allows a network attacker to persistently poison the database via HTTP with an HTTPS subresource URL, which is later automatically prefetched with same-origin credentials during a subsequent HTTPS navigation.

Affected files:

  • chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util.cc
  • chrome/browser/page_load_metrics/observers/lcp_critical_path_predictor_page_load_metrics_observer.cc
  • chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_host.cc
  • chrome/browser/predictors/prefetch_manager.cc
  • chrome/browser/predictors/perform_network_context_prefetch.cc
  • chrome/browser/predictors/loading_predictor_tab_helper.cc
  • chrome/browser/predictors/loading_predictor.cc
  • third_party/blink/renderer/core/lcp_critical_path_predictor/lcp_critical_path_predictor.cc

Estimated timestamp from git blame: 2025-10-01

Summary

The LCP Critical Path Predictor (LCPP) implementation in Chrome incorrectly keys its database records using only the host component of a URL, discarding the scheme. This architectural flaw creates a potential cross-scheme poisoning vulnerability. A network-level attacker (MITM) can inject malicious HTTPS subresource URLs into the LCPP database while a user browses the unencrypted HTTP version of a site. When the user later navigates to the secure HTTPS version of the same site, the browser automatically prefetches these attacker-controlled URLs.

Because the prefetch is triggered during the HTTPS navigation, the requests are sent as same-origin requests with SameSite=Strict cookies and Sec-Fetch-Site: same-origin headers. This effectively bypasses major CSRF defenses for state-changing GET endpoints.

Potential Attack Steps

(Note: These are suggested steps based on source code analysis; a working proof-of-concept has not been executed).

Preconditions:

  • The victim is on a network where an attacker can intercept HTTP traffic.
  • The feature blink::features::kLCPPPrefetchSubresource (and kLCPCriticalPathPredictor) is enabled (e.g., via Finch trial).
  • The target domain has state-changing GET requests protected by SameSite=Strict or Sec-Fetch-Site.

Phase 1: Poisoning via Insecure HTTP

  1. The user navigates to http://victim.com.
  2. The attacker intercepts the HTTP response and injects a subresource tag pointing to a secure endpoint, e.g., <script src="https://victim.com/api/sensitive-action"></script>.
  3. The renderer fetches the script and notifies the browser process via LCPCriticalPathPredictorHost::NotifyFetchedSubresource. This checks that the subresource is HTTP/HTTPS but does not ensure the scheme matches the document.
  4. LcpCriticalPathPredictorPageLoadMetricsObserver::AppendFetchedSubresourceUrl records the URL into lcpp_data_inputs_->subresource_urls without enforcing any IsSameSite validation against the document’s commit_url_ (http://victim.com).
  5. When the page load finalizes, LcppDataMap::LearnLcpp persists the data to the SQLite database. It generates the database key by calling GetLCPPDatabaseKey("http://victim.com"), which returns the scheme-less string "victim.com". The malicious HTTPS URL is now persistently tied to "victim.com".

Phase 2: Executing Authenticated CSRF via HTTPS Navigation 6. The user later navigates to the secure site: https://victim.com. 7. LoadingPredictorTabHelper::PrepareForPageLoad requests prefetch hints. LcppDataMap::GetPreconnectAndPrefetchRequest queries the database using GetLCPPDatabaseKey("https://victim.com"), which again evaluates to "victim.com", returning the poisoned entry. 8. The code validates the prediction using IsSameSite(https://victim.com, https://victim.com/api/sensitive-action). This passes because both URLs use the HTTPS scheme and share the same host. The cross-scheme mismatch from the write phase has been bypassed. 9. PerformNetworkContextPrefetch constructs the network request. It sets the request_initiator and site_for_cookies to the navigation origin: https://victim.com. 10. Because the target URL matches site_for_cookies, IsBannedCrossSiteAuth returns false, and the credentials_mode defaults to kInclude. 11. The Network Service dispatches the GET request. Seeing the matching site_for_cookies, it attaches SameSite=Strict and SameSite=Lax cookies. Seeing the matching request_initiator, it attaches Sec-Fetch-Site: same-origin. 12. The server receives the authenticated request and executes the state-changing action, completing the CSRF.

Suggested Fix

There are two primary ways to address this issue:

  1. Update Database Keying: GetLCPPDatabaseKey() in lcp_critical_path_predictor_util.cc should include the URL scheme (or at least differentiate between secure and insecure contexts), or
  2. Enforce Scheme/Site Matching on Write: Add an IsSameSite check against the commit_url_ within LcpCriticalPathPredictorPageLoadMetricsObserver::AppendFetchedSubresourceUrl, ensuring that an HTTP page cannot register HTTPS subresources for prefetching.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


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