Medium watch CVSS 8.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Themes
DescriptionImproper input validation in Themes
ComponentThemes
Sub-componentNtp
Bug ClassInput Validation
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.
Tracker517730821
Fix commite4e2d27bf745 (chromium/src) +74/-3
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-29 (per vendor advisory)
Fix landed2026-08-27 (public git)
Disclosed2026-09-22
Patch gap26 days — fix public in source before the advisory

Background

New Tab Page (NTP) background service
The NtpBackgroundService fetches collections of wallpaper images (and their previews/thumbnails) from a Google-hosted endpoint and exposes them to the NTP UI so the user can pick a background.
`AddOptionsToImageURL`
A helper in components/themes/ntp_background_data.cc that appends image-resize query options (e.g. =w100) to a server-provided image URL and returns it as a GURL.
`GURL::SchemeIsHTTPOrHTTPS`
A GURL predicate that returns true only when the parsed URL uses the http: or https: scheme, and false for schemes such as data:, chrome:, file:, or javascript:.
Collections/images proto responses
GetCollectionsResponse and GetImagesInCollectionResponse are protobuf messages returned by the backend whose image_url/preview fields are strings fed into AddOptionsToImageURL.

Root Cause Analysis

Before the patch, AddOptionsToImageURL took the server-supplied image_url string, optionally appended image_options, and returned GURL(...) of the concatenation with no scheme validation whatsoever. Because the string came directly from the parsed GetCollectionsResponse/GetImagesInCollectionResponse fields, any scheme the backend (or anything able to influence that response) placed in the field — data:, chrome:, file:, javascript: — was accepted verbatim and stored as an NTP image_url, preview_image_url, or thumbnail_image_url. The violated invariant is that a background/preview image URL should only ever be a remote HTTP(S) resource; the function trusted its input to already satisfy this.

The fix constructs the GURL and then calls url.SchemeIsHTTPOrHTTPS(), returning an empty GURL() on the early-exit path whenever the scheme is anything other than HTTP or HTTPS, so non-web-schemed URLs are dropped before they reach any consumer. This tightens validation: the pre-patch condition accepted all schemes, the post-patch condition accepts only HTTP(S).

Key insight
The single mistake was treating a backend-provided string as a trusted, already-safe image URL and constructing a GURL from it without checking its scheme; the fix adds an explicit SchemeIsHTTPOrHTTPS gate that collapses any non-HTTP(S) URL to an empty GURL.

Attack Path

  1. Poison the collections response An attacker who can control or tamper with the NTP background service response (e.g. a compromised/abused backend or a network position) sets a collection preview or image image_url to a non-web scheme such as data:image/png;base64,AAAA=, chrome://favicon2/?pageUrl=x, or file:///etc/passwd.
  2. URL passes through unvalidated FetchCollectionInfo/FetchCollectionImageInfo parse the proto and hand the raw string to AddOptionsToImageURL, which pre-patch returned a GURL of that string unchanged.
  3. Non-HTTP URL is stored as image source The tainted GURL is retained as a preview_image_url, thumbnail_image_url, or image_url in the collection/image info exposed to the NTP.
  4. NTP consumes the attacker-controlled scheme The NTP surface then uses that URL as a background/preview/thumbnail image source, causing the browser to act on a data:/chrome:/file:/javascript: URL in that context. (inferred)
  5. Escalation via privileged surface Depending on how the URL is loaded, this could enable local-resource reference, exfiltration, or abuse of the privileged NTP/WebUI context. (inferred)

Impact Assessment

An attacker able to influence the NTP background service response can cause non-HTTP(S) URLs (data:, chrome:, file:, javascript:) to be accepted as NTP image sources instead of only remote web images, with the precondition of controlling or tampering with that server response. The concrete downstream effect — information disclosure, local-file reference, or abuse of the privileged NTP context — depends on the image consumer and is not demonstrated by the patch itself (inferred). The advisory classifies this as improper input validation / logic error in Themes; it states no exploited-in-the-wild status.

Changed Functions

FunctionChangeNotes
TEST_F
components/themes/ntp_background_service_unittest.cc
modified

Files Changed

  • components/themes/ntp_background_data.cc
  • components/themes/ntp_background_service_unittest.cc
  • components/themes/ntp_background_data.h

Audit Directions

  • Unvalidated server-string-to-URL conversions
    Look for other spots that build a GURL from backend/proto/JSON string fields and use it as a resource source without a SchemeIsHTTPOrHTTPS (or allowlist) check.
  • NTP/WebUI-facing resource fields
    Audit the remaining image_url, preview, and thumbnail handling in NtpBackgroundService and related theme/customization code to confirm every externally-sourced URL is scheme-validated before storage or rendering.
  • Trust boundary on "trusted" backends
    Review code that assumes a Google-hosted response is inherently safe; treat all response-derived URLs and scheme fields as attacker-influenced input requiring explicit validation.
From e4e2d27bf745949b28ab4fb7964ae855c263db85 Mon Sep 17 00:00:00 2001
From: Dhruv Kathpalia <[email protected]>
Date: Wed, 26 Aug 2026 19:03:43 -0700
Subject: [PATCH] [ntp] Require HTTP or HTTPS scheme for NTP background image URLs.

This change updates AddOptionsToImageURL to validate that the generated
URL is valid and uses an HTTP or HTTPS scheme. If the URL is invalid or
uses an unsupported scheme (such as data:, chrome:, or file:), it now
returns an empty GURL. Unit tests have been added to verify this
behavior.

Fixed: b:517730821
Change-Id: Ibb3a81949bb96ecc594ddc721c186730d5380ee9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8295680
Auto-Submit: Dhruv Kathpalia <[email protected]>
Commit-Queue: Dhruv Kathpalia <[email protected]>
Commit-Queue: Tibor Goldschwendt <[email protected]>
Reviewed-by: Tibor Goldschwendt <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1686861}
---
diff --git a/components/themes/ntp_background_data.cc b/components/themes/ntp_background_data.cc
index 6dcdfd9..a201d0e 100644
--- a/components/themes/ntp_background_data.cc
+++ b/components/themes/ntp_background_data.cc
@@ -56,9 +56,13 @@
 
 GURL AddOptionsToImageURL(const std::string& image_url,
                           const std::string& image_options) {
-  return GURL(image_url + ((image_url.find('=') == std::string::npos)
-                               ? image_options
-                               : std::string("")));
+  GURL url(image_url + ((image_url.find('=') == std::string::npos)
+                            ? image_options
+                            : std::string("")));
+  if (!url.SchemeIsHTTPOrHTTPS()) {
+    return GURL();
+  }
+  return url;
 }
 
 GURL RemoveOptionsFromImageURL(const std::string& image_url) {
diff --git a/components/themes/ntp_background_data.h b/components/themes/ntp_background_data.h
index 9f6821ea..f148d0fa 100644
--- a/components/themes/ntp_background_data.h
+++ b/components/themes/ntp_background_data.h
@@ -41,6 +41,7 @@
 
 // Adds options for resizing an image to its url.
 // Without options added to the image, it is 512x512.
+// Returns an empty GURL if `image_url` is not a valid HTTP(S) URL.
 // TODO(crbug.com/41408116): Request resolution from service, instead of
 // setting it here.
 GURL AddOptionsToImageURL(const std::string& image_url,
diff --git a/components/themes/ntp_background_service_unittest.cc b/components/themes/ntp_background_service_unittest.cc
index f8b2770..c92795b 100644
--- a/components/themes/ntp_background_service_unittest.cc
+++ b/components/themes/ntp_background_service_unittest.cc
@@ -258,6 +258,72 @@
   EXPECT_EQ(service()->collection_error_info().error_type, ErrorType::NONE);
 }
 
+TEST_F(NtpBackgroundServiceTest, CollectionsResponseRejectsNonHttpPreviewUrl) {
+  ntp::background::GetCollectionsResponse response;
+  for (const std::string& url :
+       {"data:image/png;base64,AAAA=", "chrome://favicon2/?pageUrl=x",
+        "file:///etc/passwd", "javascript:alert(1)"}) {
+    ntp::background::Collection* collection = response.add_collections();
+    collection->set_collection_id("id");
+    collection->set_collection_name("name");
+    collection->add_preview()->set_image_url(url);
+  }
+  std::string response_string;
+  response.SerializeToString(&response_string);
+
+  SetUpResponseWithData(service()->GetCollectionsLoadURLForTesting(),
+                        response_string);
+
+  EXPECT_CALL(observer_, OnCollectionInfoAvailable).Times(1);
+  service()->FetchCollectionInfo();
+  base::RunLoop().RunUntilIdle();
+
+  ASSERT_EQ(4u, service()->collection_info().size());
+  for (const auto& info : service()->collection_info()) {
+    EXPECT_TRUE(info.preview_image_url.is_empty())
+        << info.preview_image_url.possibly_invalid_spec();
+  }
+}
+
+TEST_F(NtpBackgroundServiceTest,
+       CollectionImagesResponseRejectsNonHttpImageUrl) {
+  ntp::background::Image image;
+  image.set_asset_id(12345);
+  image.set_image_url("data:image/png;base64,AAAA=");
+  ntp::background::GetImagesInCollectionResponse response;
+  *response.add_images() = image;
+  std::string response_string;
+  response.SerializeToString(&response_string);
+
+  SetUpResponseWithData(service()->GetImagesURLForTesting(), response_string);
+
+  EXPECT_CALL(observer_, OnCollectionImagesAvailable).Times(1);
+  service()->FetchCollectionImageInfo("shapes");
+  base::RunLoop().RunUntilIdle();
+
+  ASSERT_EQ(1u, service()->collection_images().size());
+  EXPECT_TRUE(service()->collection_images().at(0).image_url.is_empty());
+  EXPECT_TRUE(
+      service()->collection_images().at(0).thumbnail_image_url.is_empty());
+}
+
+TEST_F(NtpBackgroundServiceTest, AddOptionsToImageURLRequiresHttpScheme) {
+  EXPECT_EQ(GURL("https://wallpapers.co/img=w100"),
+            AddOptionsToImageURL("https://wallpapers.co/img", "=w100"));
+  EXPECT_EQ(GURL("http://wallpapers.co/img=w100"),
+            AddOptionsToImageURL("http://wallpapers.co/img", "=w100"));
+  EXPECT_EQ(GURL("https://wallpapers.co/img=s"),
+            AddOptionsToImageURL("https://wallpapers.co/img=s", "=w100"));
+
+  EXPECT_TRUE(AddOptionsToImageURL("", "=w100").is_empty());
+  EXPECT_TRUE(AddOptionsToImageURL("not a url", "=w100").is_empty());
+  EXPECT_TRUE(
+      AddOptionsToImageURL("data:image/png;base64,AAAA=", "=w100").is_empty());
+  EXPECT_TRUE(AddOptionsToImageURL("chrome://favicon2/?pageUrl=x", "=w100")
+                  .is_empty());
+  EXPECT_TRUE(AddOptionsToImageURL("file:///etc/passwd", "=w100").is_empty());
+}
+
 TEST_F(NtpBackgroundServiceTest, BrokenCollectionPreviewImageHasNoReplacement) {
   ntp::background::Collection collection;
   collection.set_collection_id("shapes");
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/themes/ntp_background_service_unittest.cc b/components/themes/ntp_background_service_unittest.cc
index f8b2770..c92795b 100644
--- a/components/themes/ntp_background_service_unittest.cc
+++ b/components/themes/ntp_background_service_unittest.cc
@@ -258,6 +258,72 @@
   EXPECT_EQ(service()->collection_error_info().error_type, ErrorType::NONE);
 }
 
+TEST_F(NtpBackgroundServiceTest, CollectionsResponseRejectsNonHttpPreviewUrl) {
+  ntp::background::GetCollectionsResponse response;
+  for (const std::string& url :
+       {"data:image/png;base64,AAAA=", "chrome://favicon2/?pageUrl=x",
+        "file:///etc/passwd", "javascript:alert(1)"}) {
+    ntp::background::Collection* collection = response.add_collections();
+    collection->set_collection_id("id");
+    collection->set_collection_name("name");
+    collection->add_preview()->set_image_url(url);
+  }
+  std::string response_string;
+  response.SerializeToString(&response_string);
+
+  SetUpResponseWithData(service()->GetCollectionsLoadURLForTesting(),
+                        response_string);
+
+  EXPECT_CALL(observer_, OnCollectionInfoAvailable).Times(1);
+  service()->FetchCollectionInfo();
+  base::RunLoop().RunUntilIdle();
+
+  ASSERT_EQ(4u, service()->collection_info().size());
+  for (const auto& info : service()->collection_info()) {
+    EXPECT_TRUE(info.preview_image_url.is_empty())
+        << info.preview_image_url.possibly_invalid_spec();
+  }
+}
+
+TEST_F(NtpBackgroundServiceTest,
+       CollectionImagesResponseRejectsNonHttpImageUrl) {
+  ntp::background::Image image;
+  image.set_asset_id(12345);
+  image.set_image_url("data:image/png;base64,AAAA=");
+  ntp::background::GetImagesInCollectionResponse response;
+  *response.add_images() = image;
+  std::string response_string;
+  response.SerializeToString(&response_string);
+
+  SetUpResponseWithData(service()->GetImagesURLForTesting(), response_string);
+
+  EXPECT_CALL(observer_, OnCollectionImagesAvailable).Times(1);
+  service()->FetchCollectionImageInfo("shapes");
+  base::RunLoop().RunUntilIdle();
+
+  ASSERT_EQ(1u, service()->collection_images().size());
+  EXPECT_TRUE(service()->collection_images().at(0).image_url.is_empty());
+  EXPECT_TRUE(
+      service()->collection_images().at(0).thumbnail_image_url.is_empty());
+}
+
+TEST_F(NtpBackgroundServiceTest, AddOptionsToImageURLRequiresHttpScheme) {
+  EXPECT_EQ(GURL("https://wallpapers.co/img=w100"),
+            AddOptionsToImageURL("https://wallpapers.co/img", "=w100"));
+  EXPECT_EQ(GURL("http://wallpapers.co/img=w100"),
+            AddOptionsToImageURL("http://wallpapers.co/img", "=w100"));
+  EXPECT_EQ(GURL("https://wallpapers.co/img=s"),
+            AddOptionsToImageURL("https://wallpapers.co/img=s", "=w100"));
+
+  EXPECT_TRUE(AddOptionsToImageURL("", "=w100").is_empty());
+  EXPECT_TRUE(AddOptionsToImageURL("not a url", "=w100").is_empty());
+  EXPECT_TRUE(
+      AddOptionsToImageURL("data:image/png;base64,AAAA=", "=w100").is_empty());
+  EXPECT_TRUE(AddOptionsToImageURL("chrome://favicon2/?pageUrl=x", "=w100")
+                  .is_empty());
+  EXPECT_TRUE(AddOptionsToImageURL("file:///etc/passwd", "=w100").is_empty());
+}
+
 TEST_F(NtpBackgroundServiceTest, BrokenCollectionPreviewImageHasNoReplacement) {
   ntp::background::Collection collection;
   collection.set_collection_id("shapes");
Loading diff…

Original Bug Report

reported by [email protected]

Bypass of SanitizedImageSource in Customize Chrome WebUI via unvalidated NTP image URLs

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

Overview: A potential vulnerability exists where NtpBackgroundService does not validate the scheme of server-provided background image URLs. This allows an attacker capable of spoofing network responses to provide data: or chrome: URLs, which are forwarded to the privileged ‘Customize Chrome’ WebUI. When rendered via the <img is="cr-auto-img"> custom element, these schemes bypass the secure SanitizedImageSource out-of-process decoding mechanism and decode directly in the privileged WebUI renderer.

Affected files:

  • components/themes/ntp_background_data.cc
  • components/themes/ntp_background_service.cc
  • chrome/browser/ui/webui/side_panel/customize_chrome/customize_chrome_page_handler.cc

Estimated timestamp from git blame: 2022-12-07

Summary

A potential security validation bypass exists where NtpBackgroundService fails to validate the protocol scheme of server-supplied background image URLs. If an attacker is able to manipulate network responses from the Backdrop wallpaper service, they can inject malicious data: or chrome: URLs. When the “Customize Chrome” side panel is opened, these unvalidated URLs are forwarded via Mojo to the privileged WebUI (chrome://customize-chrome-side-panel.top-chrome/) and rendered using the <img is="cr-auto-img"> custom element. This bypasses the secure SanitizedImageSource out-of-process decoding layer, forcing the privileged WebUI renderer process to decode arbitrary image data directly.

Root Cause & Code Analysis

In components/themes/ntp_background_data.cc, AddOptionsToImageURL performs string concatenation on incoming image URL strings without verifying the scheme:

GURL AddOptionsToImageURL(const std::string& image_url,
                          const std::string& image_options) {
  return GURL(image_url + ((image_url.find('=') == std::string::npos)
                               ? image_options
                               : std::string("")));
}

If the server response contains a data: URI (e.g. data:image/png;base64,...) or a parameterized chrome: URL (e.g. chrome://favicon2/?pageUrl=...), the raw string is parsed into a GURL directly without validation.

The NtpBackgroundService parses collections and images from Google’s Backdrop service via SimpleURLLoader and populates the preview_image_url fields utilizing this function (components/themes/ntp_background_service.cc lines 211, 272). These GURLs are later passed directly to the CustomizeChromePageHandler which maps them to Mojo objects without additional validation (chrome/browser/ui/webui/side_panel/customize_chrome/customize_chrome_page_handler.cc lines 799, 838):

collection->preview_image_url = GURL(info.preview_image_url);
...
image->preview_image_url = info.thumbnail_image_url;

In the WebUI, the previewImageUrl is bound as an attribute to the custom <img is="cr-auto-img"> element (chrome/browser/resources/side_panel/customize_chrome/categories.html.ts lines 64-68).

In ui/webui/resources/cr_elements/cr_auto_img/cr_auto_img.ts (lines 93-96), the custom element explicitly allows data: and chrome: URLs to be set directly to the underlying this.src attribute to support internal resources:

if (url.protocol === 'data:' || url.protocol === 'chrome:') {
  this.src = url.href;
  return;
}

By doing so, it exits early and bypasses the secure fallback routing that would prepend chrome://image?... to direct the download and decoding to the isolated SanitizedImageSource mechanism (which executes image decoding in a highly sandboxed, low-privilege data_decoder utility process).

Potential Exploitation Path

The following outlines a potential scenario of how an attacker might trigger this path (noting that this sequence has not been executed dynamically and our testing tools do not currently have the capability to run code):

  1. The attacker gains control over network responses (e.g. via a compromised network process).
  2. The attacker intercepts the outbound request to the Backdrop wallpaper API and returns a malicious protobuf response.
  3. In the response, the attacker sets a collection’s or image’s preview URL to a data: URL containing a malformed base64-encoded image.
  4. When the user opens the “Customize Chrome” side panel, the WebUI frontend requests and receives the collections via Mojo.
  5. The WebUI binds the data: URL to a <img is="cr-auto-img"> element.
  6. The cr-auto-img element assigns the data: URL directly to this.src, bypassing the chrome://image proxy.
  7. Blink’s image decoder parses and decodes the malformed image directly within the privileged WebUI renderer process context instead of the isolated utility process. Any memory-corruption bug in the decoder would trigger in the privileged WebUI context.

Suggested Fix

We suggest adding strict scheme validation on the C++ side. NtpBackgroundService should only accept image URLs that use cryptographic schemes (e.g. SchemeIsCryptographic() or SchemeIsHTTPOrHTTPS()). Any incoming URLs that violate this check should be discarded or replaced with empty URLs.

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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