Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in WebUI
DescriptionInsufficient validation of untrusted input in WebUI
ComponentWebUI
Bug ClassLogic Error
Tracker500505339
Fix commitfe23ad63c890 (chromium/src) +74/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
WebUIURLLoaderFactoryInvalidUrlTest
content/browser/webui/web_ui_url_loader_factory_unittest.cc
modified
InvalidUrlTestBrowserClient
content/browser/webui/web_ui_url_loader_factory_unittest.cc
modified
TEST_P
content/browser/webui/web_ui_url_loader_factory_unittest.cc
modified

Files Changed

  • content/browser/webui/web_ui_url_loader_factory.cc
  • content/browser/webui/web_ui_url_loader_factory_unittest.cc
From fe23ad63c8908cd4f735d9f392d00ea6cbdf555e Mon Sep 17 00:00:00 2001
From: dpapad <dpapad@chromium.org>
Date: Tue, 14 Apr 2026 17:55:29 -0700
Subject: [PATCH] WebUI: Modify blob-internals,network-error,dino triggering logic.

Fixed: 500505339
Change-Id: I38d82744b5bfc7193c84c0c8d29fdd2e37f03d10
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7744808
Reviewed-by: Charlie Reis <creis@chromium.org>
Auto-Submit: Demetrios Papadopoulos <dpapad@chromium.org>
Commit-Queue: Charlie Reis <creis@chromium.org>
Commit-Queue: Demetrios Papadopoulos <dpapad@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1614850}
---

diff --git a/content/browser/webui/web_ui_url_loader_factory.cc b/content/browser/webui/web_ui_url_loader_factory.cc
index 8bd1801..5565410 100644
--- a/content/browser/webui/web_ui_url_loader_factory.cc
+++ b/content/browser/webui/web_ui_url_loader_factory.cc
@@ -288,7 +288,8 @@
            allowed_hosts_.find(request.url.GetHost()) != allowed_hosts_.end()))
         << "Incorrect host: " << request.url.GetHost();
 
-    if (request.url.host() == kChromeUIBlobInternalsHost) {
+    if (request.url.scheme() == kChromeUIScheme &&
+        request.url.host() == kChromeUIBlobInternalsHost) {
       GetIOThreadTaskRunner({})->PostTask(
           FROM_HERE,
           base::BindOnce(
@@ -301,8 +302,9 @@
     // This path is entered on user-trigger navigations (e.g. from omnibox or
     // links) to chrome://network-error or chrome://dino. Actual network error
     // does not trigger this path.
-    if (request.url.host() == kChromeUINetworkErrorHost ||
-        request.url.host() == kChromeUIDinoHost) {
+    if (request.url.scheme() == kChromeUIScheme &&
+        (request.url.host() == kChromeUINetworkErrorHost ||
+         request.url.host() == kChromeUIDinoHost)) {
       // Simulate a network error.
       StartNetworkErrorsURLLoader(request, std::move(client));
       // Logs WebUI usage. These WebUIs don't create a WebUI object.
diff --git a/content/browser/webui/web_ui_url_loader_factory_unittest.cc b/content/browser/webui/web_ui_url_loader_factory_unittest.cc
index 2f52a43c..f8b9ed24 100644
--- a/content/browser/webui/web_ui_url_loader_factory_unittest.cc
+++ b/content/browser/webui/web_ui_url_loader_factory_unittest.cc
@@ -6,6 +6,7 @@
 
 #include <optional>
 
+#include "base/memory/raw_ptr.h"
 #include "base/memory/ref_counted_memory.h"
 #include "base/notreached.h"
 #include "base/strings/strcat.h"
@@ -13,8 +14,11 @@
 #include "build/build_config.h"
 #include "content/browser/webui/url_data_manager.h"
 #include "content/public/browser/url_data_source.h"
+#include "content/public/common/content_client.h"
 #include "content/public/common/url_constants.h"
 #include "content/public/test/test_browser_context.h"
+#include "content/public/test/test_content_browser_client.h"
+#include "content/public/test/test_content_client.h"
 #include "content/public/test/test_renderer_host.h"
 #include "mojo/public/c/system/data_pipe.h"
 #include "mojo/public/c/system/types.h"
@@ -25,12 +29,14 @@
 #include "services/network/public/mojom/url_loader_factory.mojom.h"
 #include "services/network/test/test_url_loader_client.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "url/url_util.h"
 
 namespace content {
 
 namespace {
 
 const char* kTestWebUIScheme = kChromeUIScheme;
+const char kNonChromeDummyScheme[] = "non-chrome";
 constexpr char kTestWebUIHost[] = "testhost";
 constexpr size_t kMaxTestResourceSize = 10;
 
@@ -234,4 +240,67 @@
   }
 }
 
+class WebUIURLLoaderFactoryInvalidUrlTest
+    : public RenderViewHostTestHarness,
+      public testing::WithParamInterface<std::string> {
+ public:
+  void SetUp() override {
+    RenderViewHostTestHarness::SetUp();
+    browser_client_ = std::make_unique<InvalidUrlTestBrowserClient>();
+    old_browser_client_ = SetBrowserClientForTesting(browser_client_.get());
+  }
+
+  void TearDown() override {
+    SetBrowserClientForTesting(old_browser_client_);
+    RenderViewHostTestHarness::TearDown();
+  }
+
+ private:
+  class InvalidUrlTestBrowserClient : public TestContentBrowserClient {
+   public:
+    void GetAdditionalWebUISchemes(
+        std::vector<std::string>* additional_schemes) override {
+      additional_schemes->push_back(kNonChromeDummyScheme);
+    }
+  };
+
+  std::unique_ptr<InvalidUrlTestBrowserClient> browser_client_;
+  raw_ptr<ContentBrowserClient> old_browser_client_;
+};
+
+TEST_P(WebUIURLLoaderFactoryInvalidUrlTest, InvalidUrl) {
+  mojo::Remote<network::mojom::URLLoaderFactory> loader_factory(
+      CreateWebUIURLLoaderFactory(main_rfh(), kNonChromeDummyScheme,
+                                  /*allowed_hosts=*/{}));
+
+  network::ResourceRequest request;
+  request.url = GURL(base::StrCat({kNonChromeDummyScheme, "://", GetParam()}));
+
+  mojo::PendingRemote<network::mojom::URLLoader> loader;
+  network::TestURLLoaderClient loader_client;
+  loader_factory->CreateLoaderAndStart(
+      loader.InitWithNewPipeAndPassReceiver(), /*request_id=*/0,
+      /*options=*/0, request, loader_client.CreateRemote(),
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+  loader_client.RunUntilComplete();
+
+  EXPECT_EQ(loader_client.completion_status().error_code, net::ERR_INVALID_URL);
+}
+
+// Test that non-chrome://blob-internals, non-chrome://dino and
+// non-chrome://network-error/<xyz> are not reachable.
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    WebUIURLLoaderFactoryInvalidUrlTest,
+    testing::Values(kChromeUIBlobInternalsHost,
+                    kChromeUIDinoHost,
+                    base::StrCat({kChromeUINetworkErrorHost, "/-147"})),
+    [](const testing::TestParamInfo<std::string>& info) {
+      std::string name = base::StrCat({kNonChromeDummyScheme, "_", info.param});
+      std::replace_if(
+          name.begin(), name.end(), [](char c) { return !std::isalnum(c); },
+          '_');
+      return name;
+    });
+
 }  // namespace content
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/webui/web_ui_url_loader_factory_unittest.cc b/content/browser/webui/web_ui_url_loader_factory_unittest.cc
index 2f52a43c..f8b9ed24 100644
--- a/content/browser/webui/web_ui_url_loader_factory_unittest.cc
+++ b/content/browser/webui/web_ui_url_loader_factory_unittest.cc
@@ -6,6 +6,7 @@
 
 #include <optional>
 
+#include "base/memory/raw_ptr.h"
 #include "base/memory/ref_counted_memory.h"
 #include "base/notreached.h"
 #include "base/strings/strcat.h"
@@ -13,8 +14,11 @@
 #include "build/build_config.h"
 #include "content/browser/webui/url_data_manager.h"
 #include "content/public/browser/url_data_source.h"
+#include "content/public/common/content_client.h"
 #include "content/public/common/url_constants.h"
 #include "content/public/test/test_browser_context.h"
+#include "content/public/test/test_content_browser_client.h"
+#include "content/public/test/test_content_client.h"
 #include "content/public/test/test_renderer_host.h"
 #include "mojo/public/c/system/data_pipe.h"
 #include "mojo/public/c/system/types.h"
@@ -25,12 +29,14 @@
 #include "services/network/public/mojom/url_loader_factory.mojom.h"
 #include "services/network/test/test_url_loader_client.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "url/url_util.h"
 
 namespace content {
 
 namespace {
 
 const char* kTestWebUIScheme = kChromeUIScheme;
+const char kNonChromeDummyScheme[] = "non-chrome";
 constexpr char kTestWebUIHost[] = "testhost";
 constexpr size_t kMaxTestResourceSize = 10;
 
@@ -234,4 +240,67 @@
   }
 }
 
+class WebUIURLLoaderFactoryInvalidUrlTest
+    : public RenderViewHostTestHarness,
+      public testing::WithParamInterface<std::string> {
+ public:
+  void SetUp() override {
+    RenderViewHostTestHarness::SetUp();
+    browser_client_ = std::make_unique<InvalidUrlTestBrowserClient>();
+    old_browser_client_ = SetBrowserClientForTesting(browser_client_.get());
+  }
+
+  void TearDown() override {
+    SetBrowserClientForTesting(old_browser_client_);
+    RenderViewHostTestHarness::TearDown();
+  }
+
+ private:
+  class InvalidUrlTestBrowserClient : public TestContentBrowserClient {
+   public:
+    void GetAdditionalWebUISchemes(
+        std::vector<std::string>* additional_schemes) override {
+      additional_schemes->push_back(kNonChromeDummyScheme);
+    }
+  };
+
+  std::unique_ptr<InvalidUrlTestBrowserClient> browser_client_;
+  raw_ptr<ContentBrowserClient> old_browser_client_;
+};
+
+TEST_P(WebUIURLLoaderFactoryInvalidUrlTest, InvalidUrl) {
+  mojo::Remote<network::mojom::URLLoaderFactory> loader_factory(
+      CreateWebUIURLLoaderFactory(main_rfh(), kNonChromeDummyScheme,
+                                  /*allowed_hosts=*/{}));
+
+  network::ResourceRequest request;
+  request.url = GURL(base::StrCat({kNonChromeDummyScheme, "://", GetParam()}));
+
+  mojo::PendingRemote<network::mojom::URLLoader> loader;
+  network::TestURLLoaderClient loader_client;
+  loader_factory->CreateLoaderAndStart(
+      loader.InitWithNewPipeAndPassReceiver(), /*request_id=*/0,
+      /*options=*/0, request, loader_client.CreateRemote(),
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+  loader_client.RunUntilComplete();
+
+  EXPECT_EQ(loader_client.completion_status().error_code, net::ERR_INVALID_URL);
+}
+
+// Test that non-chrome://blob-internals, non-chrome://dino and
+// non-chrome://network-error/<xyz> are not reachable.
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    WebUIURLLoaderFactoryInvalidUrlTest,
+    testing::Values(kChromeUIBlobInternalsHost,
+                    kChromeUIDinoHost,
+                    base::StrCat({kChromeUINetworkErrorHost, "/-147"})),
+    [](const testing::TestParamInfo<std::string>& info) {
+      std::string name = base::StrCat({kNonChromeDummyScheme, "_", info.param});
+      std::replace_if(
+          name.begin(), name.end(), [](char c) { return !std::isalnum(c); },
+          '_');
+      return name;
+    });
+
 }  // namespace content
Loading diff…

Original Bug Report

reported by vm...@google.com

Profile-wide cross-origin blob metadata leak via chrome-search://blob-internals

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 security team.

Overview: A logic error in WebUIURLLoaderFactory allows a compromised Instant renderer to access the blob-internals diagnostic tool via the chrome-search:// scheme. Because the factory lacks a strict scheme check for this endpoint and the Instant process is granted an unrestricted allowed_hosts set, an attacker can leak profile-wide blob metadata, including cross-origin filesystem URLs and full local file paths.

Affected files:

  • content/browser/webui/web_ui_url_loader_factory.cc
  • chrome/browser/chrome_content_browser_client.cc
  • content/browser/blob_storage/blob_internals_url_loader.cc
  • storage/browser/blob/view_blob_internals_job.cc

Estimated timestamp from git blame: 2025-10-03

Summary

A potential vulnerability in WebUIURLLoaderFactory allows a compromised Instant renderer (such as a remote New Tab Page) to access the blob-internals diagnostic page via the chrome-search scheme. Because this factory is initialized with an empty allowed_hosts set and lacks a strict scheme check for the diagnostic endpoint, it will service requests for chrome-search://blob-internals/. This results in the disclosure of sensitive blob metadata for all origins across the user’s profile.

Technical Details

In chrome/browser/chrome_content_browser_client.cc (AddChromeSchemeFactories, lines 6344-6348), the browser process creates a WebUIURLLoaderFactory for the chrome-search scheme to support remote New Tab Pages (Instant processes). This factory is created with an empty allowed_hosts set, which explicitly acts as an allow-all mechanism for hosts under that scheme:

  if (instant_service && instant_service->IsInstantProcess(render_process_id)) {
    factories->emplace(chrome::kChromeSearchScheme,
                       content::CreateWebUIURLLoaderFactory(
                           frame_host, chrome::kChromeSearchScheme,
                           /*allowed_hosts=*/base::flat_set<std::string>()));
  }

In content/browser/webui/web_ui_url_loader_factory.cc (WebUIURLLoaderFactory::CreateLoaderAndStart), the factory validates incoming requests. While it checks that the request scheme matches the factory scheme (chrome-search), the empty allowed_hosts_ set causes the host validation check (CHECK(allowed_hosts_.empty() || ...)) at line 286 to pass immediately.

The logic then dispatches requests for blob-internals based solely on the host name, without verifying that the scheme is strictly chrome://:

    if (request.url.host() == kChromeUIBlobInternalsHost) { // kChromeUIBlobInternalsHost is "blob-internals"
      GetIOThreadTaskRunner({})->PostTask(
          FROM_HERE,
          base::BindOnce(
              &StartBlobInternalsURLLoader, request, std::move(client),
              base::Unretained(
                  ChromeBlobStorageContext::GetFor(browser_context_.get()))));
      return;
    }

Because of this missing scheme check, a request to chrome-search://blob-internals/ triggers the diagnostic loader. StartBlobInternalsURLLoader calls storage::ViewBlobInternalsJob::GenerateHTML, which iterates over the profile’s global blob registry (blob_storage_context->registry().blob_map_). This generates an HTML representation of every blob in the profile.

Impact

A compromised renderer can read this HTML response and extract:

  • Full on-disk file paths: Leaks the OS username and local paths from file-backed blobs (e.g., from <input type=file> elements in other, isolated cross-origin tabs).
  • Filesystem URLs: Leaks cross-origin internal filesystem paths.
  • Metadata: UUIDs, refcounts, content types, and sizes for all blobs in the profile.

Suggested Exploitation Steps

Note: These are potential steps; our tooling has not executed a live proof-of-concept.

  1. The attacker targets an Instant Process, for example by providing a third-party search engine that the user sets as default, which loads a remote HTTPS New Tab Page.
  2. The attacker compromises this renderer process (simulated via --enable-blink-features=MojoJS).
  3. Standard fetch() calls to chrome-search:// are blocked by Blink’s renderer-side checks and CORS. To bypass this, the attacker uses MojoJS to directly bind to the network::mojom::URLLoaderFactory interface provided in the renderer’s loader factory bundle.
  4. The attacker crafts a network::ResourceRequest for chrome-search://blob-internals/ and invokes CreateLoaderAndStart.
  5. The browser process successfully processes the request, bypassing CORS, and returns the profile-wide blob HTML via the Mojo data pipe.
  6. The attacker reads the HTML from the data pipe, extracting the sensitive cross-origin data and local file paths.

Suggested Fix

Ensure that the special-case hosts (like blob-internals, network-error, and dino) in WebUIURLLoaderFactory::CreateLoaderAndStart are only reachable when the factory’s scheme_ is strictly kChromeUIScheme (chrome://).

    if (scheme_ == content::kChromeUIScheme && request.url.host() == kChromeUIBlobInternalsHost) {

Additionally, consider explicitly restricting the allowed_hosts for the chrome-search factory to only the necessary components (e.g., most-visited, favicon, theme) rather than using an empty set.

Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234


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