Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in WebUI
DescriptionMemory corruption in WebUI
ComponentWebUI
Sub-componentWebUI
Bug ClassMemory Corruption
Tracker518138781
Fix commita1e30dd357ae (chromium/src) +257/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
chrome/browser/devtools/devtools_window_testing.cc
modified
IN_PROC_BROWSER_TEST_F
chrome/browser/devtools/devtools_browsertest.cc
modified
FailedResourceLoadObserver
chrome/browser/devtools/devtools_browsertest.cc
modified
if
chrome/browser/devtools/devtools_browsertest.cc
modified
BindLambdaForTesting
chrome/browser/devtools/devtools_browsertest.cc
modified
IN_PROC_BROWSER_TEST_F
chrome/browser/ui/webui/chrome_url_data_manager_browsertest.cc
modified
PrefService
chrome/browser/ui/webui/chrome_url_data_manager_browsertest.cc
modified
TEST_F
chrome/browser/ui/webui/devtools/devtools_ui_data_source_unittest.cc
modified
DevToolsUIDataSourceWithTaskEnvTest
chrome/browser/ui/webui/devtools/devtools_ui_data_source_unittest.cc
modified
TEST_F
chrome/browser/ui/webui/theme_source_unittest.cc
modified

Files Changed

  • chrome/browser/ui/webui/devtools/devtools_ui_data_source.cc
  • content/public/browser/url_data_source.cc
  • chrome/browser/ui/webui/devtools/devtools_ui_data_source.h
  • content/browser/webui/web_ui_data_source_unittest.cc
  • chrome/browser/devtools/devtools_browsertest.cc
  • chrome/browser/ui/webui/devtools/devtools_ui_data_source_unittest.cc
  • chrome/browser/ui/webui/chrome_url_data_manager_browsertest.cc
  • chrome/browser/ui/webui/theme_source_unittest.cc
  • chrome/browser/ui/webui/theme_source.cc
  • content/public/browser/url_data_source.h
  • chrome/browser/devtools/devtools_window_testing.cc
  • content/browser/webui/web_ui_url_loader_factory_unittest.cc
  • chrome/browser/devtools/devtools_window_testing.h
From a1e30dd357aea45fc50201080b21b0f462015a29 Mon Sep 17 00:00:00 2001
From: Devlin Cronin <rdevlin.cronin@chromium.org>
Date: Thu, 17 Sep 2026 07:38:30 -0700
Subject: [PATCH] [WebUI] Disallow devtools:// scheme for WebUI data sources by default

URLDataSource::ShouldServiceRequest() previously allowed requests from
both chrome:// and devtools:// schemes by default. Because WebUI data
sources are indexed by host in URLDataManagerBackend, a page running
under the devtools:// scheme could request sensitive chrome:// WebUI
data sources (such as devtools://prefs-internals/ or
devtools://extensions-internals/), which would then be serviced by the
corresponding chrome:// data source.

This CL updates URLDataSource::ShouldServiceRequest() to only allow
chrome:// and chrome-untrusted:// schemes by default, dropping the
default devtools:// permission. The DevToolsDataSource overrides this
to explicitly allow servicing devtools URLs.

There's a bit of a snag: devtools also relies on
devtools://theme/colors.css (which is served by the ThemeSource).

To handle this:
* Check this explicitly in ThemeSource (this is the only path needed).
* Add a new test that opens a devtools window and verifies there are no
  failed resource loads. This would catch the bug if we didn't tweak
  ThemeSource.

Add a smattering of unit and browser tests throughout.

Bug: 518138781
Change-Id: Icbaca7df64fedf38e444856baac61c65edc34a23
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8403320
Auto-Submit: Devlin Cronin <rdevlin.cronin@chromium.org>
Reviewed-by: Demetrios Papadopoulos <dpapad@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Reviewed-by: Danil Somsikov <dsv@chromium.org>
Commit-Queue: Alex Moshchuk <alexmos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699792}
---
diff --git a/chrome/browser/devtools/devtools_window_testing.cc b/chrome/browser/devtools/devtools_window_testing.cc
index 92aca1f..2a6ee91f0 100644
--- a/chrome/browser/devtools/devtools_window_testing.cc
+++ b/chrome/browser/devtools/devtools_window_testing.cc
@@ -243,9 +243,14 @@
 // DevToolsWindowCreationObserver ---------------------------------------------
 
 DevToolsWindowCreationObserver::DevToolsWindowCreationObserver()
+    : DevToolsWindowCreationObserver(base::NullCallback()) {}
+
+DevToolsWindowCreationObserver::DevToolsWindowCreationObserver(
+    base::RepeatingCallback<void(DevToolsWindow*)> on_creation_callback)
     : creation_callback_(base::BindRepeating(
           &DevToolsWindowCreationObserver::DevToolsWindowCreated,
-          base::Unretained(this))) {
+          base::Unretained(this))),
+      on_creation_callback_(std::move(on_creation_callback)) {
   DevToolsWindow::AddCreationCallbackForTest(creation_callback_);
 }
 
@@ -270,6 +275,9 @@
 void DevToolsWindowCreationObserver::DevToolsWindowCreated(
     DevToolsWindow* devtools_window) {
   devtools_windows_.push_back(devtools_window);
+  if (on_creation_callback_) {
+    on_creation_callback_.Run(devtools_window);
+  }
   if (runner_.get())
     runner_->QuitClosure().Run();
 }
diff --git a/chrome/browser/devtools/devtools_window_testing.h b/chrome/browser/devtools/devtools_window_testing.h
index 5c24062..d0310a4 100644
--- a/chrome/browser/devtools/devtools_window_testing.h
+++ b/chrome/browser/devtools/devtools_window_testing.h
@@ -91,6 +91,11 @@
  public:
   DevToolsWindowCreationObserver();
 
+  // Allows the caller to provide a custom callback that will be invoked on
+  // window creation.
+  explicit DevToolsWindowCreationObserver(
+      base::RepeatingCallback<void(DevToolsWindow*)> on_creation_callback);
+
   DevToolsWindowCreationObserver(const DevToolsWindowCreationObserver&) =
       delete;
   DevToolsWindowCreationObserver& operator=(
@@ -113,6 +118,7 @@
   void DevToolsWindowCreated(DevToolsWindow* devtools_window);
 
   base::RepeatingCallback<void(DevToolsWindow*)> creation_callback_;
+  base::RepeatingCallback<void(DevToolsWindow*)> on_creation_callback_;
   DevToolsWindows devtools_windows_;
   scoped_refptr<content::MessageLoopRunner> runner_;
 };
diff --git a/chrome/browser/ui/webui/devtools/devtools_ui_data_source.cc b/chrome/browser/ui/webui/devtools/devtools_ui_data_source.cc
index 18f1190..31bea97 100644
--- a/chrome/browser/ui/webui/devtools/devtools_ui_data_source.cc
+++ b/chrome/browser/ui/webui/devtools/devtools_ui_data_source.cc
@@ -245,6 +245,13 @@
   return true;
 }
 
+bool DevToolsDataSource::ShouldServiceRequest(
+    const GURL& url,
+    content::BrowserContext* browser_context,
+    int render_process_id) {
+  return url.SchemeIs(content::kChromeDevToolsScheme);
+}
+
 void DevToolsDataSource::StartBundledDataRequest(
     const std::string& path,
     content::URLDataSource::GotDataCallback callback) {
diff --git a/chrome/browser/ui/webui/devtools/devtools_ui_data_source.h b/chrome/browser/ui/webui/devtools/devtools_ui_data_source.h
index 8914574..7fed58d4 100644
--- a/chrome/browser/ui/webui/devtools/devtools_ui_data_source.h
+++ b/chrome/browser/ui/webui/devtools/devtools_ui_data_source.h
@@ -42,10 +42,12 @@
 
   // content::URLDataSource implementation.
   std::string GetSource() override;
-
   void StartDataRequest(const GURL& url,
                         const content::WebContents::Getter& wc_getter,
                         GotDataCallback callback) override;
+  bool ShouldServiceRequest(const GURL& url,
+                            content::BrowserContext* browser_context,
+                            int render_process_id) override;
 
  private:
   friend class DevToolsUIDataSourceTest;
diff --git a/chrome/browser/ui/webui/theme_source.cc b/chrome/browser/ui/webui/theme_source.cc
index f79e732..99a12c8 100644
--- a/chrome/browser/ui/webui/theme_source.cc
+++ b/chrome/browser/ui/webui/theme_source.cc
@@ -186,6 +186,11 @@
 bool ThemeSource::ShouldServiceRequest(const GURL& url,
                                        content::BrowserContext* browser_context,
                                        int render_process_id) {
+  if (url.SchemeIs(content::kChromeDevToolsScheme)) {
+    // Specifically allow colors.css to be accessible via devtools, but other
+    // theme resources shouldn't be.
+    return url.path() == "/colors.css";
+  }
 #if !BUILDFLAG(IS_ANDROID)
   if (url.SchemeIs(chrome::kChromeSearchScheme)) {
     return InstantService::ShouldServiceRequest(url, browser_context,
diff --git a/content/public/browser/url_data_source.cc b/content/public/browser/url_data_source.cc
index 12f7b854..78be525f 100644
--- a/content/public/browser/url_data_source.cc
+++ b/content/public/browser/url_data_source.cc
@@ -133,7 +133,11 @@
 bool URLDataSource::ShouldServiceRequest(const GURL& url,
                                          BrowserContext* browser_context,
                                          int render_process_id) {
-  return url.SchemeIs(kChromeDevToolsScheme) || url.SchemeIs(kChromeUIScheme) ||
+  // By default, allow chrome and chrome-untrusted sources.
+  // TODO(https://crbug.com/562108905): We could tighten this for
+  // chrome-untrusted so that we only service requests for chrome-untrusted if
+  // the datasource is chrome-untrusted.
+  return url.SchemeIs(kChromeUIScheme) ||
          url.SchemeIs(kChromeUIUntrustedScheme);
 }
 
diff --git a/content/public/browser/url_data_source.h b/content/public/browser/url_data_source.h
index 7992f57b..582f43f 100644
--- a/content/public/browser/url_data_source.h
+++ b/content/public/browser/url_data_source.h
@@ -107,9 +107,9 @@
   // happening, return false. It is OK to return false as needed.
   virtual bool ShouldDenyXFrameOptions();
 
-  // By default, only chrome: and devtools: requests are allowed.
+  // By default, only chrome: and chrome-untrusted: requests are allowed.
   // Override in specific WebUI data sources to enable for additional schemes or
-  // to implement fancier access control.  Typically used in concert with
+  // to implement specific access control. Typically used in concert with
   // ContentBrowserClient::GetAdditionalWebUISchemes() to permit additional
   // WebUI scheme support for an embedder.
   virtual bool ShouldServiceRequest(const GURL& url,
diff --git a/chrome/browser/devtools/devtools_browsertest.cc b/chrome/browser/devtools/devtools_browsertest.cc
index 19f15f6..26fb3001 100644
--- a/chrome/browser/devtools/devtools_browsertest.cc
+++ b/chrome/browser/devtools/devtools_browsertest.cc
@@ -105,6 +105,7 @@
 #include "content/public/browser/child_process_data.h"
 #include "content/public/browser/devtools_agent_host.h"
 #include "content/public/browser/devtools_agent_host_client.h"
+#include "content/public/browser/global_request_id.h"
 #include "content/public/browser/navigation_controller.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_widget_host.h"
@@ -113,6 +114,7 @@
 #include "content/public/browser/storage_partition.h"
 #include "content/public/browser/url_data_source.h"
 #include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_contents_observer.h"
 #include "content/public/browser/web_ui_controller.h"
 #include "content/public/common/content_features.h"
 #include "content/public/common/content_switches.h"
@@ -128,6 +130,7 @@
 #include "extensions/buildflags/buildflags.h"
 #include "mojo/public/cpp/bindings/sync_call_restrictions.h"
 #include "net/base/filename_util.h"
+#include "net/base/net_errors.h"
 #include "net/dns/mock_host_resolver.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "net/test/embedded_test_server/http_request.h"
@@ -140,6 +143,7 @@
 #include "third_party/blink/public/common/chrome_debug_urls.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/common/input/web_input_event.h"
+#include "third_party/blink/public/mojom/loader/resource_load_info.mojom.h"
 #include "ui/base/page_transition_types.h"
 #include "ui/base/ui_base_switches.h"
 #include "ui/base/window_open_disposition.h"
@@ -4789,6 +4793,111 @@
                   "a JavaScript error: \"TypeError: Failed to fetch\n")));
 }
 
+// Tests that devtools is not allowed to fetch a host that's registered with a
+// different scheme. Regression test for https://crbug.com/518138781.
+IN_PROC_BROWSER_TEST_F(DevToolsFetchTest,
+                       DevToolsFetchDifferentHostDisallowed) {
+  OpenDevToolsWindow("about:blank", true);
+
+  const auto prefs_result =
+      FetchFromDevToolsWindow("devtools://prefs-internals/");
+  EXPECT_THAT(prefs_result,
+              content::EvalJsResult::ErrorIs(::testing::StartsWith(
+                  "a JavaScript error: \"TypeError: Failed to fetch\n")));
+
+  const auto extensions_result =
+      FetchFromDevToolsWindow("devtools://extensions-internals/");
+  EXPECT_THAT(extensions_result,
+              content::EvalJsResult::ErrorIs(::testing::StartsWith(
+                  "a JavaScript error: \"TypeError: Failed to fetch\n")));
+
+  CloseDevToolsWindow();
+}
+
+// These tests exercise that the devtools windows can access theme colors, but
+// the ThemeSource is not yet included on android builds. As such, exclude
+// these tests on android builds.
+#if !BUILDFLAG(IS_ANDROID)
+// Tests that devtools *is* allowed to fetch theme colors.
+IN_PROC_BROWSER_TEST_F(DevToolsFetchTest, DevToolsThemeColorsCssAllowed) {
+  OpenDevToolsWindow("about:blank", true);
+  WebContents* wc = DevToolsWindowTesting::Get(window_)->main_web_contents();
+
+  // Theme colors should be accessible by devtools.
+  EXPECT_EQ(true, content::EvalJs(wc, R"(
+    new Promise((resolve) => {
+      const link = document.createElement('link');
+      link.rel = 'stylesheet';
+      link.href = 'devtools://theme/colors.css?sets=ui,chrome';
+      link.onload = () => resolve(true);
+      link.onerror = () => resolve(false);
+      document.body.appendChild(link);
+    });
+  )"));
+
+  // By contrast, other resources should not be.
+  EXPECT_EQ(false, content::EvalJs(wc, R"(
+    new Promise((resolve) => {
+      const link = document.createElement('link');
+      link.rel = 'stylesheet';
+      link.href = 'devtools://theme/IDR_SETTINGS_FAVICON';
+      link.onload = () => resolve(true);
+      link.onerror = () => resolve(false);
+      document.body.appendChild(link);
+    });
+  )"));
+
+  CloseDevToolsWindow();
+}
+
+namespace {
+
+// Observes resource loads on a WebContents and tracks any requests that fail
+// with a network error.
+class FailedResourceLoadObserver : public content::WebContentsObserver {
+ public:
+  explicit FailedResourceLoadObserver(content::WebContents* web_contents)
+      : content::WebContentsObserver(web_contents) {}
+
+  void ResourceLoadComplete(
+      content::RenderFrameHost* render_frame_host,
+      const content::GlobalRequestID& request_id,
+      const GURL& original_url,
+      const blink::mojom::ResourceLoadInfo& resource_load_info) override {
+    if (resource_load_info.net_error != net::OK) {
+      failed_urls_.push_back(original_url);
+    }
+  }
+
+  const std::vector<GURL>& failed_urls() const { return failed_urls_; }
+
+ private:
+  std::vector<GURL> failed_urls_;
+};
+
+}  // namespace
+
+// Tests that there are no failed resources for a newly-opened devtools window.
+// This test helps catch any cases where we might have tweaked a security
+// setting that would result in devtools failing to have access to all the
+// resources it needed.
+IN_PROC_BROWSER_TEST_F(DevToolsTest, NoFailedResourceLoadsOnStartup) {
+  std::unique_ptr<FailedResourceLoadObserver> load_observer;
+  DevToolsWindowCreationObserver observer(
+      base::BindLambdaForTesting([&](DevToolsWindow* window) {
+        load_observer = std::make_unique<FailedResourceLoadObserver>(
+            DevToolsWindowTesting::Get(window)->main_web_contents());
+      }));
+
+  OpenDevToolsWindow("about:blank", /*is_docked=*/true);
+
+  ASSERT_TRUE(load_observer);
+  EXPECT_THAT(load_observer->failed_urls(), ::testing::IsEmpty());
+
+  CloseDevToolsWindow();
+}
+#endif  // !BUILDFLAG(IS_ANDROID)
+
 IN_PROC_BROWSER_TEST_F(DevToolsTest, HostBindingsSyncIntegration) {
   // Smoke test to make sure that `registerPreference` works from JavaScript.
   OpenDevToolsWindow("about:blank", true);
diff --git a/chrome/browser/ui/webui/chrome_url_data_manager_browsertest.cc b/chrome/browser/ui/webui/chrome_url_data_manager_browsertest.cc
index ea8b571..f6bb2e8 100644
--- a/chrome/browser/ui/webui/chrome_url_data_manager_browsertest.cc
+++ b/chrome/browser/ui/webui/chrome_url_data_manager_browsertest.cc
@@ -154,6 +154,18 @@
   EXPECT_NE(net::OK, observer.net_error());
 }
 
+// Makes sure resources registered for chrome: are not serviced under other
+// schemes.
+IN_PROC_BROWSER_TEST_F(ChromeURLDataManagerTest, DifferentSchemeNotServiced) {
+  NavigationObserver observer(
+      browser()->tab_strip_model()->GetActiveWebContents());
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), GURL("devtools://theme/IDR_SETTINGS_FAVICON")));
+  EXPECT_EQ(NavigationObserver::Result::kErrorPage,
+            observer.navigation_result());
+  EXPECT_NE(net::OK, observer.net_error());
+}
+
 #if BUILDFLAG(IS_CHROMEOS)
 class PrefService;
 #endif
diff --git a/chrome/browser/ui/webui/devtools/devtools_ui_data_source_unittest.cc b/chrome/browser/ui/webui/devtools/devtools_ui_data_source_unittest.cc
index 5ba4f78..b4e129e 100644
--- a/chrome/browser/ui/webui/devtools/devtools_ui_data_source_unittest.cc
+++ b/chrome/browser/ui/webui/devtools/devtools_ui_data_source_unittest.cc
@@ -363,6 +363,18 @@
                                base::CompareCase::SENSITIVE));
 }
 
+TEST_F(DevToolsUIDataSourceTest, ShouldServiceRequest) {
+  EXPECT_TRUE(data_source()->ShouldServiceRequest(
+      GURL("devtools://devtools/bundled/devtools_app.html"), nullptr, -1));
+  EXPECT_FALSE(data_source()->ShouldServiceRequest(
+      GURL("chrome://devtools/bundled/devtools_app.html"), nullptr, -1));
+  EXPECT_FALSE(data_source()->ShouldServiceRequest(
+      GURL("chrome-untrusted://devtools/bundled/devtools_app.html"), nullptr,
+      -1));
+  EXPECT_FALSE(data_source()->ShouldServiceRequest(
+      GURL("https://devtools/bundled/devtools_app.html"), nullptr, -1));
+}
+
 class DevToolsUIDataSourceWithTaskEnvTest : public testing::Test {
  public:
   DevToolsUIDataSourceWithTaskEnvTest()
diff --git a/chrome/browser/ui/webui/theme_source_unittest.cc b/chrome/browser/ui/webui/theme_source_unittest.cc
index 5cc065c..f612380 100644
--- a/chrome/browser/ui/webui/theme_source_unittest.cc
+++ b/chrome/browser/ui/webui/theme_source_unittest.cc
@@ -161,3 +161,29 @@
       theme_source()->GetAccessControlAllowOriginForOrigin("http://google.com"),
       "");
 }
+
+TEST_F(WebUISourcesTest, ThemeSourceShouldServiceRequest) {
+  EXPECT_TRUE(theme_source()->ShouldServiceRequest(
+      GURL("chrome://theme/colors.css"), nullptr, -1));
+  EXPECT_TRUE(theme_source()->ShouldServiceRequest(
+      GURL("chrome://theme/IDR_THEME_FRAME"), nullptr, -1));
+
+  EXPECT_TRUE(theme_source()->ShouldServiceRequest(
+      GURL("devtools://theme/colors.css"), nullptr, -1));
+  EXPECT_TRUE(theme_source()->ShouldServiceRequest(
+      GURL("devtools://theme/colors.css?sets=ui,chrome"), nullptr, -1));
+
+  EXPECT_FALSE(theme_source()->ShouldServiceRequest(
+      GURL("devtools://theme/IDR_THEME_FRAME"), nullptr, -1));
+  EXPECT_FALSE(theme_source()->ShouldServiceRequest(
+      GURL("devtools://theme/css/new_tab_theme.css"), nullptr, -1));
+  EXPECT_FALSE(theme_source()->ShouldServiceRequest(GURL("devtools://theme/"),
+                                                    nullptr, -1));
+  // TODO(https://crbug.com/562108905): URLDataSource currently allows both
+  // chrome: and chrome-untrusted: requests by default. We should tighten this
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/devtools/devtools_browsertest.cc b/chrome/browser/devtools/devtools_browsertest.cc
index 19f15f6..26fb3001 100644
--- a/chrome/browser/devtools/devtools_browsertest.cc
+++ b/chrome/browser/devtools/devtools_browsertest.cc
@@ -105,6 +105,7 @@
 #include "content/public/browser/child_process_data.h"
 #include "content/public/browser/devtools_agent_host.h"
 #include "content/public/browser/devtools_agent_host_client.h"
+#include "content/public/browser/global_request_id.h"
 #include "content/public/browser/navigation_controller.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_widget_host.h"
@@ -113,6 +114,7 @@
 #include "content/public/browser/storage_partition.h"
 #include "content/public/browser/url_data_source.h"
 #include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_contents_observer.h"
 #include "content/public/browser/web_ui_controller.h"
 #include "content/public/common/content_features.h"
 #include "content/public/common/content_switches.h"
@@ -128,6 +130,7 @@
 #include "extensions/buildflags/buildflags.h"
 #include "mojo/public/cpp/bindings/sync_call_restrictions.h"
 #include "net/base/filename_util.h"
+#include "net/base/net_errors.h"
 #include "net/dns/mock_host_resolver.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "net/test/embedded_test_server/http_request.h"
@@ -140,6 +143,7 @@
 #include "third_party/blink/public/common/chrome_debug_urls.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/common/input/web_input_event.h"
+#include "third_party/blink/public/mojom/loader/resource_load_info.mojom.h"
 #include "ui/base/page_transition_types.h"
 #include "ui/base/ui_base_switches.h"
 #include "ui/base/window_open_disposition.h"
@@ -4789,6 +4793,111 @@
                   "a JavaScript error: \"TypeError: Failed to fetch\n")));
 }
 
+// Tests that devtools is not allowed to fetch a host that's registered with a
+// different scheme. Regression test for https://crbug.com/518138781.
+IN_PROC_BROWSER_TEST_F(DevToolsFetchTest,
+                       DevToolsFetchDifferentHostDisallowed) {
+  OpenDevToolsWindow("about:blank", true);
+
+  const auto prefs_result =
+      FetchFromDevToolsWindow("devtools://prefs-internals/");
+  EXPECT_THAT(prefs_result,
+              content::EvalJsResult::ErrorIs(::testing::StartsWith(
+                  "a JavaScript error: \"TypeError: Failed to fetch\n")));
+
+  const auto extensions_result =
+      FetchFromDevToolsWindow("devtools://extensions-internals/");
+  EXPECT_THAT(extensions_result,
+              content::EvalJsResult::ErrorIs(::testing::StartsWith(
+                  "a JavaScript error: \"TypeError: Failed to fetch\n")));
+
+  CloseDevToolsWindow();
+}
+
+// These tests exercise that the devtools windows can access theme colors, but
+// the ThemeSource is not yet included on android builds. As such, exclude
+// these tests on android builds.
+#if !BUILDFLAG(IS_ANDROID)
+// Tests that devtools *is* allowed to fetch theme colors.
+IN_PROC_BROWSER_TEST_F(DevToolsFetchTest, DevToolsThemeColorsCssAllowed) {
+  OpenDevToolsWindow("about:blank", true);
+  WebContents* wc = DevToolsWindowTesting::Get(window_)->main_web_contents();
+
+  // Theme colors should be accessible by devtools.
+  EXPECT_EQ(true, content::EvalJs(wc, R"(
+    new Promise((resolve) => {
+      const link = document.createElement('link');
+      link.rel = 'stylesheet';
+      link.href = 'devtools://theme/colors.css?sets=ui,chrome';
+      link.onload = () => resolve(true);
+      link.onerror = () => resolve(false);
+      document.body.appendChild(link);
+    });
+  )"));
+
+  // By contrast, other resources should not be.
+  EXPECT_EQ(false, content::EvalJs(wc, R"(
+    new Promise((resolve) => {
+      const link = document.createElement('link');
+      link.rel = 'stylesheet';
+      link.href = 'devtools://theme/IDR_SETTINGS_FAVICON';
+      link.onload = () => resolve(true);
+      link.onerror = () => resolve(false);
+      document.body.appendChild(link);
+    });
+  )"));
+
+  CloseDevToolsWindow();
+}
+
+namespace {
+
+// Observes resource loads on a WebContents and tracks any requests that fail
+// with a network error.
+class FailedResourceLoadObserver : public content::WebContentsObserver {
+ public:
+  explicit FailedResourceLoadObserver(content::WebContents* web_contents)
+      : content::WebContentsObserver(web_contents) {}
+
+  void ResourceLoadComplete(
+      content::RenderFrameHost* render_frame_host,
+      const content::GlobalRequestID& request_id,
+      const GURL& original_url,
+      const blink::mojom::ResourceLoadInfo& resource_load_info) override {
+    if (resource_load_info.net_error != net::OK) {
+      failed_urls_.push_back(original_url);
+    }
+  }
+
+  const std::vector<GURL>& failed_urls() const { return failed_urls_; }
+
+ private:
+  std::vector<GURL> failed_urls_;
+};
+
+}  // namespace
+
+// Tests that there are no failed resources for a newly-opened devtools window.
+// This test helps catch any cases where we might have tweaked a security
+// setting that would result in devtools failing to have access to all the
+// resources it needed.
+IN_PROC_BROWSER_TEST_F(DevToolsTest, NoFailedResourceLoadsOnStartup) {
+  std::unique_ptr<FailedResourceLoadObserver> load_observer;
+  DevToolsWindowCreationObserver observer(
+      base::BindLambdaForTesting([&](DevToolsWindow* window) {
+        load_observer = std::make_unique<FailedResourceLoadObserver>(
+            DevToolsWindowTesting::Get(window)->main_web_contents());
+      }));
+
+  OpenDevToolsWindow("about:blank", /*is_docked=*/true);
+
+  ASSERT_TRUE(load_observer);
+  EXPECT_THAT(load_observer->failed_urls(), ::testing::IsEmpty());
+
+  CloseDevToolsWindow();
+}
+#endif  // !BUILDFLAG(IS_ANDROID)
+
 IN_PROC_BROWSER_TEST_F(DevToolsTest, HostBindingsSyncIntegration) {
   // Smoke test to make sure that `registerPreference` works from JavaScript.
   OpenDevToolsWindow("about:blank", true);
diff --git a/chrome/browser/ui/webui/chrome_url_data_manager_browsertest.cc b/chrome/browser/ui/webui/chrome_url_data_manager_browsertest.cc
index ea8b571..f6bb2e8 100644
--- a/chrome/browser/ui/webui/chrome_url_data_manager_browsertest.cc
+++ b/chrome/browser/ui/webui/chrome_url_data_manager_browsertest.cc
@@ -154,6 +154,18 @@
   EXPECT_NE(net::OK, observer.net_error());
 }
 
+// Makes sure resources registered for chrome: are not serviced under other
+// schemes.
+IN_PROC_BROWSER_TEST_F(ChromeURLDataManagerTest, DifferentSchemeNotServiced) {
+  NavigationObserver observer(
+      browser()->tab_strip_model()->GetActiveWebContents());
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), GURL("devtools://theme/IDR_SETTINGS_FAVICON")));
+  EXPECT_EQ(NavigationObserver::Result::kErrorPage,
+            observer.navigation_result());
+  EXPECT_NE(net::OK, observer.net_error());
+}
+
 #if BUILDFLAG(IS_CHROMEOS)
 class PrefService;
 #endif
diff --git a/chrome/browser/ui/webui/devtools/devtools_ui_data_source_unittest.cc b/chrome/browser/ui/webui/devtools/devtools_ui_data_source_unittest.cc
index 5ba4f78..b4e129e 100644
--- a/chrome/browser/ui/webui/devtools/devtools_ui_data_source_unittest.cc
+++ b/chrome/browser/ui/webui/devtools/devtools_ui_data_source_unittest.cc
@@ -363,6 +363,18 @@
                                base::CompareCase::SENSITIVE));
 }
 
+TEST_F(DevToolsUIDataSourceTest, ShouldServiceRequest) {
+  EXPECT_TRUE(data_source()->ShouldServiceRequest(
+      GURL("devtools://devtools/bundled/devtools_app.html"), nullptr, -1));
+  EXPECT_FALSE(data_source()->ShouldServiceRequest(
+      GURL("chrome://devtools/bundled/devtools_app.html"), nullptr, -1));
+  EXPECT_FALSE(data_source()->ShouldServiceRequest(
+      GURL("chrome-untrusted://devtools/bundled/devtools_app.html"), nullptr,
+      -1));
+  EXPECT_FALSE(data_source()->ShouldServiceRequest(
+      GURL("https://devtools/bundled/devtools_app.html"), nullptr, -1));
+}
+
 class DevToolsUIDataSourceWithTaskEnvTest : public testing::Test {
  public:
   DevToolsUIDataSourceWithTaskEnvTest()
diff --git a/chrome/browser/ui/webui/theme_source_unittest.cc b/chrome/browser/ui/webui/theme_source_unittest.cc
index 5cc065c..f612380 100644
--- a/chrome/browser/ui/webui/theme_source_unittest.cc
+++ b/chrome/browser/ui/webui/theme_source_unittest.cc
@@ -161,3 +161,29 @@
       theme_source()->GetAccessControlAllowOriginForOrigin("http://google.com"),
       "");
 }
+
+TEST_F(WebUISourcesTest, ThemeSourceShouldServiceRequest) {
+  EXPECT_TRUE(theme_source()->ShouldServiceRequest(
+      GURL("chrome://theme/colors.css"), nullptr, -1));
+  EXPECT_TRUE(theme_source()->ShouldServiceRequest(
+      GURL("chrome://theme/IDR_THEME_FRAME"), nullptr, -1));
+
+  EXPECT_TRUE(theme_source()->ShouldServiceRequest(
+      GURL("devtools://theme/colors.css"), nullptr, -1));
+  EXPECT_TRUE(theme_source()->ShouldServiceRequest(
+      GURL("devtools://theme/colors.css?sets=ui,chrome"), nullptr, -1));
+
+  EXPECT_FALSE(theme_source()->ShouldServiceRequest(
+      GURL("devtools://theme/IDR_THEME_FRAME"), nullptr, -1));
+  EXPECT_FALSE(theme_source()->ShouldServiceRequest(
+      GURL("devtools://theme/css/new_tab_theme.css"), nullptr, -1));
+  EXPECT_FALSE(theme_source()->ShouldServiceRequest(GURL("devtools://theme/"),
+                                                    nullptr, -1));
+  // TODO(https://crbug.com/562108905): URLDataSource currently allows both
+  // chrome: and chrome-untrusted: requests by default. We should tighten this
+  // so that chrome: sources only service chrome: requests.
+  EXPECT_TRUE(theme_source()->ShouldServiceRequest(
+      GURL("chrome-untrusted://theme/colors.css"), nullptr, -1));
+  EXPECT_FALSE(theme_source()->ShouldServiceRequest(
+      GURL("https://theme/colors.css"), nullptr, -1));
+}
diff --git a/content/browser/webui/web_ui_data_source_unittest.cc b/content/browser/webui/web_ui_data_source_unittest.cc
index 3722a9f..d92ad20da 100644
--- a/content/browser/webui/web_ui_data_source_unittest.cc
+++ b/content/browser/webui/web_ui_data_source_unittest.cc
@@ -494,6 +494,35 @@
             url::Origin::Create(GURL("chrome-untrusted://host")));
 }
 
+TEST_F(WebUIDataSourceTest, ShouldServiceRequest) {
+  CreateDataSource("host");
+  URLDataSource* chrome_source = source()->source();
+  EXPECT_TRUE(
+      chrome_source->ShouldServiceRequest(GURL("chrome://host/"), nullptr, -1));
+  EXPECT_FALSE(chrome_source->ShouldServiceRequest(GURL("devtools://host/"),
+                                                   nullptr, -1));
+  // TODO(https://crbug.com/562108905): URLDataSource currently allows both
+  // chrome: and chrome-untrusted: requests by default. We should tighten this
+  // to only allow the matching scheme.
+  EXPECT_TRUE(chrome_source->ShouldServiceRequest(
+      GURL("chrome-untrusted://host/"), nullptr, -1));
+  EXPECT_FALSE(
+      chrome_source->ShouldServiceRequest(GURL("https://host/"), nullptr, -1));
+
+  CreateDataSource("chrome-untrusted://host/");
+  URLDataSource* untrusted_source = source()->source();
+  EXPECT_TRUE(untrusted_source->ShouldServiceRequest(
+      GURL("chrome-untrusted://host/"), nullptr, -1));
+  // TODO(https://crbug.com/562108905): Tighten this to only allow the matching
+  // scheme.
+  EXPECT_TRUE(untrusted_source->ShouldServiceRequest(GURL("chrome://host/"),
+                                                     nullptr, -1));
+  EXPECT_FALSE(untrusted_source->ShouldServiceRequest(GURL("devtools://host/"),
+                                                      nullptr, -1));
+  EXPECT_FALSE(untrusted_source->ShouldServiceRequest(GURL("https://host/"),
+                                                      nullptr, -1));
+}
+
 #if BUILDFLAG(LOAD_WEBUI_FROM_DISK)
 // LoadWebUIFromDiskTest does not run on any bots, only meant to run locally,
 // since it tests a feature only used during local development and guarded by a
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 8c1cdbac7..e112017fa 100644
--- a/content/browser/webui/web_ui_url_loader_factory_unittest.cc
+++ b/content/browser/webui/web_ui_url_loader_factory_unittest.cc
@@ -13,6 +13,7 @@
 #include "base/strings/stringprintf.h"
 #include "build/build_config.h"
 #include "content/browser/webui/url_data_manager.h"
+#include "content/browser/webui/url_data_manager_backend.h"
 #include "content/public/browser/url_data_source.h"
 #include "content/public/common/content_client.h"
 #include "content/public/common/url_constants.h"
@@ -230,14 +231,19 @@
       public testing::WithParamInterface<std::string> {
  public:
   void SetUp() override {
+    URLDataManagerBackend::SetDisallowWebUISchemeCachingForTesting(true);
     RenderViewHostTestHarness::SetUp();
     browser_client_ = std::make_unique<InvalidUrlTestBrowserClient>();
     old_browser_client_ = SetBrowserClientForTesting(browser_client_.get());
+    URLDataManager::AddDataSource(
+        browser_context(),
+        std::make_unique<TestWebUIDataSource>(kMaxTestResourceSize));
   }
 
   void TearDown() override {
     SetBrowserClientForTesting(old_browser_client_);
     RenderViewHostTestHarness::TearDown();
+    URLDataManagerBackend::SetDisallowWebUISchemeCachingForTesting(false);
   }
 
  private:
... (truncated)
Loading diff…

Original Bug Report

reported by vm...@google.com

Scheme-bypass in URLDataManagerBackend lets DevTools read sensitive chrome:// WebUI data

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 logical vulnerability in URLDataManagerBackend allows WebUI data sources to be resolved purely by host name, ignoring the request’s scheme. This can allow a compromised devtools:// renderer to bypass scheme isolation boundaries and request sensitive chrome://-exclusive data sources. Consequently, an attacker could potentially extract user preferences and extension metadata from the browser process.

Affected files:

  • content/browser/webui/url_data_manager_backend.cc
  • content/browser/webui/web_ui_url_loader_factory.cc
  • content/public/browser/url_data_source.cc
  • chrome/browser/ui/webui/prefs_internals_source.cc
  • chrome/browser/ui/webui/extensions/extensions_internals_source.cc

Estimated timestamp from git blame: 2018-03-01

Root Cause Analysis

There is a potential logical security boundary bypass in how WebUI URL data sources are resolved and validated across different schemes in the browser process:

  1. Host-Only Lookup in URLDataManagerBackend: In URLDataManagerBackend::GetDataSourceFromURL (content/browser/webui/url_data_manager_backend.cc), standard (non-chrome-untrusted://) requests are resolved purely by host name, completely ignoring the scheme of the requested URL:

    // content/browser/webui/url_data_manager_backend.cc
    auto i = data_sources_.find(url.GetHost());
    if (i != data_sources_.end())
      return i->second.get();
    

    As a result, a request targeting devtools://prefs-internals/ or devtools://extensions-internals/ successfully resolves to the browser-registered PrefsInternalsSource or ExtensionsInternalsSource, despite carrying a devtools:// scheme.

  2. Permissive Default Scheme Handling: Neither PrefsInternalsSource nor ExtensionsInternalsSource overrides the ShouldServiceRequest validation check. Consequently, execution falls back to the default implementation in URLDataSource::ShouldServiceRequest (content/public/browser/url_data_source.cc), which explicitly trusts and permits requests from devtools:// (kChromeDevToolsScheme):

    bool URLDataSource::ShouldServiceRequest(const GURL& url,
                                             BrowserContext* browser_context,
                                             int render_process_id) {
      return url.SchemeIs(kChromeDevToolsScheme) || url.SchemeIs(kChromeUIScheme) ||
             url.SchemeIs(kChromeUIUntrustedScheme);
    }
    
  3. Mojo Loader Factory Configuration: When a devtools:// frame commits navigation, RenderFrameHostImpl::CommitNavigation (content/browser/renderer_host/render_frame_host_impl.cc) instantiates a WebUIURLLoaderFactory for the devtools scheme with an empty allowed_hosts_ filter. Since allowed_hosts_ is empty, the factory allows requests to any host name under that scheme, bypassing host-filtering in CreateLoaderAndStart (content/browser/webui/web_ui_url_loader_factory.cc).

Potential Attack Scenario / Trigger Path

Note: These are suggested/potential steps compiled from static code tracing; our tooling agent does not currently have the capability to run code to verify with a live proof of concept.

  1. A user opens DevTools, resulting in a frame committing navigation to devtools://devtools/bundled/devtools_app.html. The browser grants this renderer process a WebUIURLLoaderFactory bound to the devtools scheme with no host constraints.
  2. An attacker compromises the sandboxed devtools:// renderer process (e.g., via a compromised renderer, DOM-XSS on a remote-hosted DevTools front-end, or a malicious DevTools-extension panel).
  3. The attacker issues a direct Mojo request (or standard asynchronous fetch()) to the registered WebUIURLLoaderFactory targeting devtools://prefs-internals/ or devtools://extensions-internals/.
  4. On the browser side, the WebUIURLLoaderFactory passes scheme validation because the requested scheme matches its own (devtools). The empty allowed_hosts_ check succeeds.
  5. URLDataManagerBackend::GetDataSourceFromURL extracts the host name (e.g., 'prefs-internals') and returns a pointer to the matching PrefsInternalsSource.
  6. The browser checks ShouldServiceRequest, which evaluates to true because the request scheme is 'devtools'.
  7. The browser process executes the data request, compiling sensitive data (user preferences, account details, sync configurations, or extension metadata) and streaming it back to the compromised renderer over the Mojo data pipe.

Security Impact

An attacker in possession of a compromised devtools:// renderer could read highly sensitive configuration and configuration metadata, bypassing renderer sandbox restrictions for data access. Specifically, this leaks:

  • Full user profile preferences (PrefsInternalsSource): including user email addresses, synchronization states, per-origin content settings, and metadata.
  • Extension lists (ExtensionsInternalsSource): leaking installed extensions, permissions, and extension file paths.

Suggested Fix

To prevent scheme-agnostic routing:

  1. Update URLDataManagerBackend::GetDataSourceFromURL to verify that the incoming URL’s scheme matches the scheme expected by the resolved URLDataSource.
  2. Or, more restrictively, override ShouldServiceRequest on sensitive internal sources such as PrefsInternalsSource and ExtensionsInternalsSource to return true only if the requested scheme is explicitly chrome:// (kChromeUIScheme), rather than defaulting to the base implementation that permits the devtools:// scheme.

Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040


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