Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in History
DescriptionMemory corruption in History
ComponentHistory
Sub-componentNtp_tiles
Bug ClassMemory Corruption
Tracker504073211
Fix commit1c4ae16a042a (chromium/src) +120/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
components/ntp_tiles/popular_sites_impl.cc
modified
TEST_F
components/ntp_tiles/popular_sites_impl_unittest.cc
modified

Files Changed

  • components/ntp_tiles/resources/default_popular_sites.json
  • components/ntp_tiles/popular_sites_impl_unittest.cc
  • components/ntp_tiles/popular_sites_impl.cc
From 1c4ae16a042aaed30e431a08d80a4e87ed4b0a6e Mon Sep 17 00:00:00 2001
From: Mike Taylor <miketaylr@chromium.org>
Date: Mon, 21 Sep 2026 19:18:52 -0700
Subject: [PATCH] [ntp_tiles] Restrict popular sites and icon URLs to HTTP(S)

Enforce that popular site URLs must be valid HTTP or HTTPS URLs in
ParseSiteList(), skipping any entries with other schemes or invalid
URLs. Clear favicon_url and large_icon_url fields if they are invalid or
not HTTP(S).

Also update default_popular_sites.json to replace legacy chrome:// URLs
with valid HTTPS URLs so default tile counts remain intact.

Bug: 504073211
Change-Id: I898f5d6b969422b0799861725d58c314f64566b3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8428305
Reviewed-by: Tibor Goldschwendt <tiborg@chromium.org>
Commit-Queue: Mike Taylor <miketaylr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1702318}
---
diff --git a/components/ntp_tiles/popular_sites_impl.cc b/components/ntp_tiles/popular_sites_impl.cc
index e2b522ed..c4f0a77 100644
--- a/components/ntp_tiles/popular_sites_impl.cc
+++ b/components/ntp_tiles/popular_sites_impl.cc
@@ -41,6 +41,7 @@
 #include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/cpp/shared_url_loader_factory.h"
 #include "services/network/public/cpp/simple_url_loader.h"
+#include "url/gurl.h"
 
 #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
 #include "components/grit/components_resources.h"
@@ -153,19 +154,26 @@
     } else {
       continue;
     }
-    std::string url;
+    GURL url;
     if (const std::string* ptr = item.FindString("url")) {
-      url = *ptr;
-    } else {
+      url = GURL(*ptr);
+    }
+    if (!url.SchemeIsHTTPOrHTTPS()) {
       continue;
     }
-    std::string favicon_url;
+    GURL favicon_url;
     if (const std::string* ptr = item.FindString("favicon_url")) {
-      favicon_url = *ptr;
+      favicon_url = GURL(*ptr);
+      if (!favicon_url.SchemeIsHTTPOrHTTPS()) {
+        favicon_url = GURL();
+      }
     }
-    std::string large_icon_url;
+    GURL large_icon_url;
     if (const std::string* ptr = item.FindString("large_icon_url")) {
-      large_icon_url = *ptr;
+      large_icon_url = GURL(*ptr);
+      if (!large_icon_url.SchemeIsHTTPOrHTTPS()) {
+        large_icon_url = GURL();
+      }
     }
 
     TileTitleSource title_source = TileTitleSource::UNKNOWN;
@@ -178,8 +186,8 @@
       title_source = static_cast<TileTitleSource>(*title_source_int);
     }
 
-    sites.emplace_back(title, GURL(url), GURL(favicon_url),
-                       GURL(large_icon_url), title_source);
+    sites.emplace_back(title, std::move(url), std::move(favicon_url),
+                       std::move(large_icon_url), title_source);
     std::optional<int> default_icon_resource =
         item.FindInt("default_icon_resource");
     if (default_icon_resource) {
diff --git a/components/ntp_tiles/resources/default_popular_sites.json b/components/ntp_tiles/resources/default_popular_sites.json
index f193c18..bccd28c87 100644
--- a/components/ntp_tiles/resources/default_popular_sites.json
+++ b/components/ntp_tiles/resources/default_popular_sites.json
@@ -31,12 +31,12 @@
     },
     {
         "large_icon_url": "https://cdn2.iconfinder.com/data/icons/minimalism/512/Chromium.png",
-        "title": "Chrome Flags",
-        "url": "chrome://flags"
+        "title": "Chromium Issues",
+        "url": "https://issues.chromium.org/"
     },
     {
         "large_icon_url": "https://cdn2.iconfinder.com/data/icons/minimalism/512/Chromium.png",
-        "title": "Chrome Version",
-        "url": "chrome://version/"
+        "title": "Chromium Source",
+        "url": "https://source.chromium.org/"
     }
 ]
diff --git a/components/ntp_tiles/popular_sites_impl_unittest.cc b/components/ntp_tiles/popular_sites_impl_unittest.cc
index 29cfb38..d389463 100644
--- a/components/ntp_tiles/popular_sites_impl_unittest.cc
+++ b/components/ntp_tiles/popular_sites_impl_unittest.cc
@@ -581,5 +581,104 @@
   EXPECT_THAT(sections, Not(Contains(Pair(SectionType::NEWS, _))));
 }
 
+TEST_F(PopularSitesTest, IgnoresEntriesWithNonWebUrls) {
+  const TestPopularSite kChromeSettings{
+      {kTitle, "Chrome Settings"},
+      {kUrl, "chrome://settings"},
+  };
+  const TestPopularSite kLocalFile{
+      {kTitle, "Local File"},
+      {kUrl, "file:///sdcard/Download/leaked_note.txt"},
+  };
+  const TestPopularSite kJavascript{
+      {kTitle, "JavaScript"},
+      {kUrl, "javascript:alert(1)"},
+  };
+  const TestPopularSite kIntent{
+      {kTitle, "Intent"},
+      {kUrl, "intent://example.com/#Intent;scheme=https;end"},
+  };
+  const TestPopularSite kInvalidUrl{
+      {kTitle, "Invalid URL"},
+      {kUrl, "not a valid url"},
+  };
+  const TestPopularSite kDataUrl{
+      {kTitle, "Data URL"},
+      {kUrl, "data:text/html,<html></html>"},
+  };
+  const TestPopularSite kAboutBlank{
+      {kTitle, "About Blank"},
+      {kUrl, "about:blank"},
+  };
+
+  SetCountryAndVersion("ZZ", "7");
+  RespondWithV5JSON(
+      "https://www.gstatic.com/chrome/ntp/suggested_sites_ZZ_7.json",
+      {kChromeSettings, kLocalFile, kWikipedia, kJavascript, kIntent,
+       kInvalidUrl, kDataUrl, kAboutBlank, kYouTube});
+
+  PopularSites::SitesVector sites;
+  EXPECT_THAT(FetchPopularSites(/*force_download=*/true, &sites),
+              Eq(std::optional<bool>(true)));
+
+  ASSERT_THAT(sites.size(), Eq(2u));
+  EXPECT_THAT(sites[0].url, URLEq("https://zz.m.wikipedia.org/"));
+  EXPECT_THAT(sites[1].url, URLEq("https://m.youtube.com/"));
+}
+
+TEST_F(PopularSitesTest, IgnoresNonWebIconUrls) {
+  const TestPopularSite kSiteWithNonWebIcons{
+      {kTitle, "Site With Non-Web Icons"},
+      {kUrl, "https://example.com/"},
+      {kFaviconUrl, "chrome://favicon2/https://example.com"},
+      {kLargeIconUrl, "javascript:alert(1)"},
+  };
+  const TestPopularSite kSiteWithFileAndDataIcons{
+      {kTitle, "Site With File And Data Icons"},
+      {kUrl, "https://example.org/"},
+      {kFaviconUrl, "file:///sdcard/favicon.ico"},
+      {kLargeIconUrl, "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg=="},
+  };
+  const TestPopularSite kSiteWithInvalidIcons{
+      {kTitle, "Site With Invalid Icons"},
+      {kUrl, "https://example.net/"},
+      {kFaviconUrl, "not_a_valid_url"},
+      {kLargeIconUrl, "also not valid"},
+  };
+  const TestPopularSite kSiteWithWebIcons{
+      {kTitle, "Site With Web Icons"},
+      {kUrl, "https://example.info/"},
+      {kFaviconUrl, "https://example.info/favicon.ico"},
+      {kLargeIconUrl, "http://example.info/icon.png"},
+  };
+
+  SetCountryAndVersion("ZZ", "7");
+  RespondWithV5JSON(
+      "https://www.gstatic.com/chrome/ntp/suggested_sites_ZZ_7.json",
+      {kSiteWithNonWebIcons, kSiteWithFileAndDataIcons, kSiteWithInvalidIcons,
+       kSiteWithWebIcons});
+
+  PopularSites::SitesVector sites;
+  EXPECT_THAT(FetchPopularSites(/*force_download=*/true, &sites),
+              Eq(std::optional<bool>(true)));
+
+  ASSERT_THAT(sites.size(), Eq(4u));
+  EXPECT_THAT(sites[0].url, URLEq("https://example.com/"));
+  EXPECT_THAT(sites[0].favicon_url, URLEq(""));
+  EXPECT_THAT(sites[0].large_icon_url, URLEq(""));
+
+  EXPECT_THAT(sites[1].url, URLEq("https://example.org/"));
+  EXPECT_THAT(sites[1].favicon_url, URLEq(""));
+  EXPECT_THAT(sites[1].large_icon_url, URLEq(""));
+
+  EXPECT_THAT(sites[2].url, URLEq("https://example.net/"));
+  EXPECT_THAT(sites[2].favicon_url, URLEq(""));
+  EXPECT_THAT(sites[2].large_icon_url, URLEq(""));
+
+  EXPECT_THAT(sites[3].url, URLEq("https://example.info/"));
+  EXPECT_THAT(sites[3].favicon_url, URLEq("https://example.info/favicon.ico"));
+  EXPECT_THAT(sites[3].large_icon_url, URLEq("http://example.info/icon.png"));
+}
+
 }  // namespace
 }  // namespace ntp_tiles
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/ntp_tiles/popular_sites_impl_unittest.cc b/components/ntp_tiles/popular_sites_impl_unittest.cc
index 29cfb38..d389463 100644
--- a/components/ntp_tiles/popular_sites_impl_unittest.cc
+++ b/components/ntp_tiles/popular_sites_impl_unittest.cc
@@ -581,5 +581,104 @@
   EXPECT_THAT(sections, Not(Contains(Pair(SectionType::NEWS, _))));
 }
 
+TEST_F(PopularSitesTest, IgnoresEntriesWithNonWebUrls) {
+  const TestPopularSite kChromeSettings{
+      {kTitle, "Chrome Settings"},
+      {kUrl, "chrome://settings"},
+  };
+  const TestPopularSite kLocalFile{
+      {kTitle, "Local File"},
+      {kUrl, "file:///sdcard/Download/leaked_note.txt"},
+  };
+  const TestPopularSite kJavascript{
+      {kTitle, "JavaScript"},
+      {kUrl, "javascript:alert(1)"},
+  };
+  const TestPopularSite kIntent{
+      {kTitle, "Intent"},
+      {kUrl, "intent://example.com/#Intent;scheme=https;end"},
+  };
+  const TestPopularSite kInvalidUrl{
+      {kTitle, "Invalid URL"},
+      {kUrl, "not a valid url"},
+  };
+  const TestPopularSite kDataUrl{
+      {kTitle, "Data URL"},
+      {kUrl, "data:text/html,<html></html>"},
+  };
+  const TestPopularSite kAboutBlank{
+      {kTitle, "About Blank"},
+      {kUrl, "about:blank"},
+  };
+
+  SetCountryAndVersion("ZZ", "7");
+  RespondWithV5JSON(
+      "https://www.gstatic.com/chrome/ntp/suggested_sites_ZZ_7.json",
+      {kChromeSettings, kLocalFile, kWikipedia, kJavascript, kIntent,
+       kInvalidUrl, kDataUrl, kAboutBlank, kYouTube});
+
+  PopularSites::SitesVector sites;
+  EXPECT_THAT(FetchPopularSites(/*force_download=*/true, &sites),
+              Eq(std::optional<bool>(true)));
+
+  ASSERT_THAT(sites.size(), Eq(2u));
+  EXPECT_THAT(sites[0].url, URLEq("https://zz.m.wikipedia.org/"));
+  EXPECT_THAT(sites[1].url, URLEq("https://m.youtube.com/"));
+}
+
+TEST_F(PopularSitesTest, IgnoresNonWebIconUrls) {
+  const TestPopularSite kSiteWithNonWebIcons{
+      {kTitle, "Site With Non-Web Icons"},
+      {kUrl, "https://example.com/"},
+      {kFaviconUrl, "chrome://favicon2/https://example.com"},
+      {kLargeIconUrl, "javascript:alert(1)"},
+  };
+  const TestPopularSite kSiteWithFileAndDataIcons{
+      {kTitle, "Site With File And Data Icons"},
+      {kUrl, "https://example.org/"},
+      {kFaviconUrl, "file:///sdcard/favicon.ico"},
+      {kLargeIconUrl, "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg=="},
+  };
+  const TestPopularSite kSiteWithInvalidIcons{
+      {kTitle, "Site With Invalid Icons"},
+      {kUrl, "https://example.net/"},
+      {kFaviconUrl, "not_a_valid_url"},
+      {kLargeIconUrl, "also not valid"},
+  };
+  const TestPopularSite kSiteWithWebIcons{
+      {kTitle, "Site With Web Icons"},
+      {kUrl, "https://example.info/"},
+      {kFaviconUrl, "https://example.info/favicon.ico"},
+      {kLargeIconUrl, "http://example.info/icon.png"},
+  };
+
+  SetCountryAndVersion("ZZ", "7");
+  RespondWithV5JSON(
+      "https://www.gstatic.com/chrome/ntp/suggested_sites_ZZ_7.json",
+      {kSiteWithNonWebIcons, kSiteWithFileAndDataIcons, kSiteWithInvalidIcons,
+       kSiteWithWebIcons});
+
+  PopularSites::SitesVector sites;
+  EXPECT_THAT(FetchPopularSites(/*force_download=*/true, &sites),
+              Eq(std::optional<bool>(true)));
+
+  ASSERT_THAT(sites.size(), Eq(4u));
+  EXPECT_THAT(sites[0].url, URLEq("https://example.com/"));
+  EXPECT_THAT(sites[0].favicon_url, URLEq(""));
+  EXPECT_THAT(sites[0].large_icon_url, URLEq(""));
+
+  EXPECT_THAT(sites[1].url, URLEq("https://example.org/"));
+  EXPECT_THAT(sites[1].favicon_url, URLEq(""));
+  EXPECT_THAT(sites[1].large_icon_url, URLEq(""));
+
+  EXPECT_THAT(sites[2].url, URLEq("https://example.net/"));
+  EXPECT_THAT(sites[2].favicon_url, URLEq(""));
+  EXPECT_THAT(sites[2].large_icon_url, URLEq(""));
+
+  EXPECT_THAT(sites[3].url, URLEq("https://example.info/"));
+  EXPECT_THAT(sites[3].favicon_url, URLEq("https://example.info/favicon.ico"));
+  EXPECT_THAT(sites[3].large_icon_url, URLEq("http://example.info/icon.png"));
+}
+
 }  // namespace
 }  // namespace ntp_tiles
Loading diff…

Original Bug Report

reported by vm...@google.com

Privilege escalation via unvalidated schemes in NTP Popular Sites

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 go/chrome-ai-generated-security-bugs-faq for more information.

Overview: The PopularSitesImpl component fails to validate URL schemes when parsing remote JSON for New Tab Page suggested sites. A compromised network process could inject tiles with privileged schemes like chrome:// or file://. When tapped by a user, the browser initiates the navigation, bypassing standard security checks to load the privileged context.

Affected files:

  • components/ntp_tiles/popular_sites_impl.cc
  • chrome/android/java/src/org/chromium/chrome/browser/suggestions/tile/TileGroupDelegateImpl.java
  • chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsNavigationDelegate.java
  • chrome/android/java/src/org/chromium/chrome/browser/native_page/NativePageFactory.java
  • chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java

Estimated timestamp from git blame: 2017-01-27

Summary

On Android, PopularSitesImpl fetches a list of popular sites from a remote JSON endpoint. The parsing logic in PopularSitesImpl::ParseSiteList extracts URLs from this JSON and creates GURL objects without validating their schemes.

If the network process is compromised, an attacker can serve a malicious JSON response containing URLs with privileged schemes such as chrome:// or file://. These URLs are persisted and displayed as tiles on the New Tab Page (NTP). When a user taps one of these tiles, the navigation is performed as a browser-initiated load, bypassing standard ChildProcessSecurityPolicy restrictions that prevent renderer-initiated navigations to privileged URLs.

Technical Details

The core issue resides in components/ntp_tiles/popular_sites_impl.cc within ParseSiteList. The code extracts the url string from the JSON and creates a GURL object without any scheme validation:

// components/ntp_tiles/popular_sites_impl.cc
std::string url;
if (const std::string* ptr = item.FindString("url")) {
  url = *ptr;
}
// ...
sites.emplace_back(title, GURL(url), GURL(favicon_url), GURL(large_icon_url), title_source);

On Android, the navigation flow for these tiles proceeds as follows:

  1. A tap event triggers TileGroupDelegateImpl.openMostVisitedItem.
  2. The URL is passed to SuggestionsNavigationDelegate.navigateToSuggestionUrl, which builds LoadUrlParams with PageTransition.AUTO_BOOKMARK.
  3. The request is routed to TabImpl.loadUrl, which passes the parameters across JNI to the C++ NavigationController.

Because the navigation is initiated by the Java UI (loadUrl), it is treated as a browser-initiated navigation (is_renderer_initiated = false). This natively bypasses renderer-side IPC restrictions and ChildProcessSecurityPolicyImpl::CanRequestURL checks. The browser will successfully commit navigations to standard WebUI pages (e.g., chrome://settings) or local file paths (file://), acting as a privilege escalation primitive.

Note: These are potential steps based on static code analysis; our tooling agent does not yet have the ability to run live exploit code to demonstrate a full chain.

Potential Reproduction Steps

  1. An attacker compromises the network process on an Android device.
  2. When the browser process requests the suggested sites JSON, the attacker returns a malicious payload: [{"title":"Settings","url":"chrome://settings","large_icon_url":"https://example.com/icon.ico"}]
  3. The tile appears on the Android NTP and is persisted in the user’s profile preferences.
  4. The user opens a New Tab and taps the injected tile.
  5. Chrome navigates to the privileged chrome://settings page, bypassing normal cross-process navigation blocks.

Enforce url.SchemeIsHTTPOrHTTPS() validation in ParseSiteList for the url, favicon_url, and large_icon_url fields. Any entries with invalid or privileged schemes should be safely discarded during parsing.

Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646


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