Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement DevTools
DescriptionInsufficient policy enforcement DevTools
ComponentChromium
Bug ClassLogic Error
Tracker517086161
Fix commitf11a3a6fed51 (chromium/src) +66/-70
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-11

Changed Functions

FunctionChangeNotes
DevToolsUIBindings
chrome/browser/devtools/devtools_ui_bindings.cc
modified
if
chrome/browser/devtools/devtools_ui_bindings.cc
modified
BindLambdaForTesting
chrome/browser/devtools/devtools_ui_bindings_unittest.cc
modified
for
chrome/browser/devtools/devtools_ui_bindings_unittest.cc
modified

Files Changed

  • chrome/browser/devtools/devtools_ui_bindings.cc
  • chrome/browser/devtools/devtools_ui_bindings_unittest.cc
From f11a3a6fed510555ac2588a9d7b6c47b7492ec08 Mon Sep 17 00:00:00 2001
From: Yulun Zeng <yulunz@chromium.org>
Date: Wed, 03 Jun 2026 14:26:16 -0700
Subject: [PATCH] Fix file scheme guard bypass for proxied remote frontends.

The URL is rewritten to have a devtools:// scheme when the devtools UI
is served remote. The insecure file read comes from loading the file
content using the JS binding (`InspectorFrontendHost`), which is only
exposed when the scheme is devtools://. So we don't have to explicitly
handle the https:// case anymore. Nevertheless, this is re-written such
that we only file read if the devtools URL is one of known local
variations.

Bug: 517086161
Change-Id: I7fc75af78b21b2aac3fad4950f7b9ae383677345
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7881922
Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
Commit-Queue: Yulun Zeng <yulunz@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1641213}
---

diff --git a/chrome/browser/devtools/devtools_ui_bindings.cc b/chrome/browser/devtools/devtools_ui_bindings.cc
index b2e6a02..167ef2b1 100644
--- a/chrome/browser/devtools/devtools_ui_bindings.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings.cc
@@ -61,6 +61,7 @@
 #include "chrome/common/chrome_switches.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/common/url_constants.h"
+#include "chrome/common/webui_url_constants.h"
 #include "chrome/grit/generated_resources.h"
 #include "components/content_settings/core/common/features.h"
 #include "components/infobars/content/content_infobar_manager.h"
@@ -535,6 +536,21 @@
                              base::Value(encoded));
 }
 
+bool IsLocalDevToolsFrontendURL(const GURL& url) {
+  if (!url.is_valid() || url.IsAboutBlank() ||
+      !url.SchemeIs(content::kChromeDevToolsScheme) ||
+      url.host() != chrome::kChromeUIDevToolsHost) {
+    return false;
+  }
+  std::string_view path = url.path();
+  if (base::StartsWith(path, "/")) {
+    path = path.substr(1);
+  }
+  return base::StartsWith(path, chrome::kChromeUIDevToolsBundledPath) ||
+         base::StartsWith(path, chrome::kChromeUIDevToolsCustomPath) ||
+         base::StartsWith(path, chrome::kChromeUIDevToolsBlankPath);
+}
+
 }  // namespace
 
 class DevToolsUIBindings::NetworkResourceLoader
@@ -1274,10 +1290,7 @@
   NetworkResourceLoader::URLLoaderFactoryHolder url_loader_factory;
   if (gurl.SchemeIsFile()) {
     GURL frontend_url = web_contents_->GetLastCommittedURL();
-    bool is_remote_frontend =
-        frontend_url.is_valid() && !frontend_url.IsAboutBlank() &&
-        IsValidRemoteFrontendURL(frontend_url);
-    if (is_remote_frontend) {
+    if (!IsLocalDevToolsFrontendURL(frontend_url)) {
       if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
               switches::kAllowUnsafeDevToolsRemoteFileLoading)) {
         base::DictValue response_dict;
diff --git a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
index 0ac76a9..1cd4fa7a 100644
--- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
@@ -106,41 +106,12 @@
 };
 
 TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
-       BlocksFileSchemeFromRemoteFrontend) {
-  // Simulate a remote frontend URL.
-  GURL remote_url(
-      "https://chrome-devtools-frontend.appspot.com/serve_rev/@12345/"
-      "inspector.html");
-  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
-                                                             remote_url);
-
-  base::RunLoop run_loop;
-  base::DictValue result;
-
-  CallLoadNetworkResource(
-      "file:///etc/passwd", "", 0,
-      base::BindLambdaForTesting([&](const base::Value* value) {
-        result = value->GetDict().Clone();
-        run_loop.Quit();
-      }));
-  run_loop.Run();
-
-  EXPECT_EQ(result.FindInt("statusCode"), 403);
-  ASSERT_NE(result.FindString("messageOverride"), nullptr);
-  EXPECT_EQ(*result.FindString("messageOverride"),
-            "Local file loading is restricted for remote DevTools. Use "
-            "--allow-unsafe-devtools-remote-file-loading to enable it.");
-}
-
-TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
        AllowsFileSchemeFromRemoteFrontendWithFlag) {
   base::test::ScopedCommandLine scoped_command_line;
   scoped_command_line.GetProcessCommandLine()->AppendSwitch(
       switches::kAllowUnsafeDevToolsRemoteFileLoading);
 
-  GURL remote_url(
-      "https://chrome-devtools-frontend.appspot.com/serve_rev/@12345/"
-      "inspector.html");
+  GURL remote_url("devtools://devtools/remote/serve_rev/@12345/inspector.html");
   content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
                                                              remote_url);
 
@@ -161,35 +132,42 @@
 }
 
 TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
-       AllowsFileSchemeFromLocalFrontend) {
-  GURL local_url("devtools://devtools/bundled/devtools_app.html");
-  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
-                                                             local_url);
+       AllowsFileSchemeFromLocalFrontends) {
+  std::vector<GURL> local_urls = {
+      GURL("devtools://devtools/bundled/devtools_app.html"),
+      GURL("devtools://devtools/custom/inspector.html"),
+      GURL("devtools://devtools/blank")};
 
-  base::RunLoop run_loop;
-  base::DictValue result;
+  for (const GURL& local_url : local_urls) {
+    content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                               local_url);
 
-  CallLoadNetworkResource(
-      "file:///etc/passwd", "", 0,
-      base::BindLambdaForTesting([&](const base::Value* value) {
-        result = value->GetDict().Clone();
-        run_loop.Quit();
-      }));
-  run_loop.Run();
+    base::RunLoop run_loop;
+    base::DictValue result;
 
-  auto* msg = result.FindString("messageOverride");
-  EXPECT_EQ(msg, nullptr);
-  EXPECT_NE(result.FindInt("statusCode"), 403);
+    CallLoadNetworkResource(
+        "file:///etc/passwd", "", 0,
+        base::BindLambdaForTesting([&](const base::Value* value) {
+          result = value->GetDict().Clone();
+          run_loop.Quit();
+        }));
+    run_loop.Run();
+
+    auto* msg = result.FindString("messageOverride");
+    EXPECT_EQ(msg, nullptr);
+    EXPECT_NE(result.FindInt("statusCode"), 403);
+  }
 }
 
 TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
-       BlocksFileSchemeFromRemoteFrontendWithLocalTarget) {
-  GURL remote_url(
-      "https://chrome-devtools-frontend.appspot.com/serve_rev/@12345/"
-      "inspector.html");
-  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
-                                                             remote_url);
+       BlocksFileSchemeFromUntrustedFrontends) {
+  std::vector<GURL> untrusted_urls = {
+      GURL("devtools://devtools/remote/serve_rev/@12345/inspector.html"),
+      GURL("https://chrome-devtools-frontend.appspot.com/serve_rev/@12345/"
+           "inspector.html"),
+      GURL("https://example.com/index.html")};
 
+  // Set up inspected WebContents to be a local file.
   content::WebContents* inspected_web_contents =
       web_contents_factory_.CreateWebContents(profile_.get());
   content::NavigationSimulator::NavigateAndCommitFromBrowser(
@@ -199,22 +177,27 @@
       std::make_unique<MockDevToolsUIBindingsDelegate>(inspected_web_contents);
   bindings()->SetDelegate(delegate.release());
 
-  base::RunLoop run_loop;
-  base::DictValue result;
+  for (const GURL& untrusted_url : untrusted_urls) {
+    content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                               untrusted_url);
 
-  CallLoadNetworkResource(
-      "file:///etc/passwd", "", 0,
-      base::BindLambdaForTesting([&](const base::Value* value) {
-        result = value->GetDict().Clone();
-        run_loop.Quit();
-      }));
-  run_loop.Run();
+    base::RunLoop run_loop;
+    base::DictValue result;
 
-  EXPECT_EQ(result.FindInt("statusCode"), 403);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
index 0ac76a9..1cd4fa7a 100644
--- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
@@ -106,41 +106,12 @@
 };
 
 TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
-       BlocksFileSchemeFromRemoteFrontend) {
-  // Simulate a remote frontend URL.
-  GURL remote_url(
-      "https://chrome-devtools-frontend.appspot.com/serve_rev/@12345/"
-      "inspector.html");
-  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
-                                                             remote_url);
-
-  base::RunLoop run_loop;
-  base::DictValue result;
-
-  CallLoadNetworkResource(
-      "file:///etc/passwd", "", 0,
-      base::BindLambdaForTesting([&](const base::Value* value) {
-        result = value->GetDict().Clone();
-        run_loop.Quit();
-      }));
-  run_loop.Run();
-
-  EXPECT_EQ(result.FindInt("statusCode"), 403);
-  ASSERT_NE(result.FindString("messageOverride"), nullptr);
-  EXPECT_EQ(*result.FindString("messageOverride"),
-            "Local file loading is restricted for remote DevTools. Use "
-            "--allow-unsafe-devtools-remote-file-loading to enable it.");
-}
-
-TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
        AllowsFileSchemeFromRemoteFrontendWithFlag) {
   base::test::ScopedCommandLine scoped_command_line;
   scoped_command_line.GetProcessCommandLine()->AppendSwitch(
       switches::kAllowUnsafeDevToolsRemoteFileLoading);
 
-  GURL remote_url(
-      "https://chrome-devtools-frontend.appspot.com/serve_rev/@12345/"
-      "inspector.html");
+  GURL remote_url("devtools://devtools/remote/serve_rev/@12345/inspector.html");
   content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
                                                              remote_url);
 
@@ -161,35 +132,42 @@
 }
 
 TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
-       AllowsFileSchemeFromLocalFrontend) {
-  GURL local_url("devtools://devtools/bundled/devtools_app.html");
-  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
-                                                             local_url);
+       AllowsFileSchemeFromLocalFrontends) {
+  std::vector<GURL> local_urls = {
+      GURL("devtools://devtools/bundled/devtools_app.html"),
+      GURL("devtools://devtools/custom/inspector.html"),
+      GURL("devtools://devtools/blank")};
 
-  base::RunLoop run_loop;
-  base::DictValue result;
+  for (const GURL& local_url : local_urls) {
+    content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                               local_url);
 
-  CallLoadNetworkResource(
-      "file:///etc/passwd", "", 0,
-      base::BindLambdaForTesting([&](const base::Value* value) {
-        result = value->GetDict().Clone();
-        run_loop.Quit();
-      }));
-  run_loop.Run();
+    base::RunLoop run_loop;
+    base::DictValue result;
 
-  auto* msg = result.FindString("messageOverride");
-  EXPECT_EQ(msg, nullptr);
-  EXPECT_NE(result.FindInt("statusCode"), 403);
+    CallLoadNetworkResource(
+        "file:///etc/passwd", "", 0,
+        base::BindLambdaForTesting([&](const base::Value* value) {
+          result = value->GetDict().Clone();
+          run_loop.Quit();
+        }));
+    run_loop.Run();
+
+    auto* msg = result.FindString("messageOverride");
+    EXPECT_EQ(msg, nullptr);
+    EXPECT_NE(result.FindInt("statusCode"), 403);
+  }
 }
 
 TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
-       BlocksFileSchemeFromRemoteFrontendWithLocalTarget) {
-  GURL remote_url(
-      "https://chrome-devtools-frontend.appspot.com/serve_rev/@12345/"
-      "inspector.html");
-  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
-                                                             remote_url);
+       BlocksFileSchemeFromUntrustedFrontends) {
+  std::vector<GURL> untrusted_urls = {
+      GURL("devtools://devtools/remote/serve_rev/@12345/inspector.html"),
+      GURL("https://chrome-devtools-frontend.appspot.com/serve_rev/@12345/"
+           "inspector.html"),
+      GURL("https://example.com/index.html")};
 
+  // Set up inspected WebContents to be a local file.
   content::WebContents* inspected_web_contents =
       web_contents_factory_.CreateWebContents(profile_.get());
   content::NavigationSimulator::NavigateAndCommitFromBrowser(
@@ -199,22 +177,27 @@
       std::make_unique<MockDevToolsUIBindingsDelegate>(inspected_web_contents);
   bindings()->SetDelegate(delegate.release());
 
-  base::RunLoop run_loop;
-  base::DictValue result;
+  for (const GURL& untrusted_url : untrusted_urls) {
+    content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                               untrusted_url);
 
-  CallLoadNetworkResource(
-      "file:///etc/passwd", "", 0,
-      base::BindLambdaForTesting([&](const base::Value* value) {
-        result = value->GetDict().Clone();
-        run_loop.Quit();
-      }));
-  run_loop.Run();
+    base::RunLoop run_loop;
+    base::DictValue result;
 
-  EXPECT_EQ(result.FindInt("statusCode"), 403);
-  ASSERT_NE(result.FindString("messageOverride"), nullptr);
-  EXPECT_EQ(*result.FindString("messageOverride"),
-            "Local file loading is restricted for remote DevTools. Use "
-            "--allow-unsafe-devtools-remote-file-loading to enable it.");
+    CallLoadNetworkResource(
+        "file:///etc/passwd", "", 0,
+        base::BindLambdaForTesting([&](const base::Value* value) {
+          result = value->GetDict().Clone();
+          run_loop.Quit();
+        }));
+    run_loop.Run();
+
+    EXPECT_EQ(result.FindInt("statusCode"), 403);
+    ASSERT_NE(result.FindString("messageOverride"), nullptr);
+    EXPECT_EQ(*result.FindString("messageOverride"),
+              "Local file loading is restricted for remote DevTools. Use "
+              "--allow-unsafe-devtools-remote-file-loading to enable it.");
+  }
 }
 
 TEST_F(DevToolsUIBindingsTest, SanitizeFrontendURL) {
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential file:// scheme guard bypass in DevToolsUIBindings::LoadNetworkResource

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 logic error in DevToolsUIBindings::LoadNetworkResource prevents the file:// scheme guard from ever triggering in production remote debugging sessions. This could potentially allow an attacker who controls a debugging target to read arbitrary local files via a custom or compromised DevTools frontend. A testing gap in unit tests masked this issue by simulating direct navigations that do not occur in production.

Affected files:

  • chrome/browser/devtools/devtools_ui_bindings.cc
  • chrome/browser/devtools/devtools_ui_bindings_unittest.cc

Estimated timestamp from git blame: 2026-04-22

Root Cause & Analysis

A security guard exists in DevToolsUIBindings::LoadNetworkResource to prevent remote (CDN-hosted) DevTools frontends from loading local system files (file:// scheme URLs). However, in production environments, this guard is ineffective due to a mismatch between how remote URLs are proxied and how the guard detects them.

In chrome/browser/devtools/devtools_ui_bindings.cc:

if (gurl.SchemeIsFile()) {
  GURL frontend_url = web_contents_->GetLastCommittedURL();
  bool is_remote_frontend = 
      frontend_url.is_valid() && !frontend_url.IsAboutBlank() &&
      IsValidRemoteFrontendURL(frontend_url);
  if (is_remote_frontend) {
    if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
            switches::kAllowUnsafeDevToolsRemoteFileLoading)) {
      // Blocks the load (403 ERR_ACCESS_DENIED)
      return;
    }
  }
  // Bypassed: An unrestricted FileURLLoaderFactory is created
  mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote =
      content::CreateFileURLLoaderFactory(...);
  ...
}

The guard checks whether the committed URL of the DevTools page is a remote frontend URL using IsValidRemoteFrontendURL:

bool DevToolsUIBindings::IsValidRemoteFrontendURL(const GURL& url) {
  return ::SanitizeFrontendURL(url, url::kHttpsScheme, kRemoteFrontendDomain,
                               url.GetPath(), true)
             .spec() == url.spec();
}

IsValidRemoteFrontendURL reconstructs the URL with the https:// scheme and chrome-devtools-frontend.appspot.com host, returning true only if the original URL is an https:// URL.

Why the Guard is Bypassed in Production

In a real remote debugging session, the committed URL of the frame never has an https:// scheme:

  1. URL Proxying: When debugging a remote target, DevToolsWindow::OpenDevToolsWindowForRemoteTarget redirects the target-supplied appspot URL using DevToolsUI::GetProxyURL (in chrome/browser/ui/webui/devtools/devtools_ui.cc), rewriting it into a devtools:// scheme: devtools://devtools/remote/serve_rev/@<hash>/<file>.html?remoteFrontend=true
  2. Scheme Mismatch: Because the committed URL has a devtools:// scheme, the comparison in IsValidRemoteFrontendURL between the reconstructed https:// URL and the input devtools:// URL always fails. Consequently, is_remote_frontend evaluates to false, and the protection is bypassed.

Testing Gap

Existing unit tests (such as BlocksFileSchemeFromRemoteFrontend in devtools_ui_bindings_unittest.cc) navigate directly to an https://chrome-devtools-frontend.appspot.com/... URL. In production, such a direct HTTPS navigation would reset frontend_host_ and fail to initialize DevTools bindings, but the mock testing environment allows this path, creating a testing blind spot.


Potential Exploitation Scenario

Note: The following steps are potential/suggested vector steps as our tooling does not yet have the capability to execute code.

  1. An attacker controls or compromises a remote debugging target (e.g., via USB/ADB or an exposed TCP port).
  2. The attacker serves a target metadata endpoint containing a "devtoolsFrontendUrl" pointing to an older, vulnerable revision of the DevTools frontend hosted on the Google appspot CDN (e.g., https://chrome-devtools-frontend.appspot.com/serve_rev/@<hash>/inspector.html).
  3. The victim opens chrome://inspect and inspects the target.
  4. The browser proxies the URL to devtools://devtools/remote/... and initializes the bindings.
  5. The attacker’s compromised/manipulated frontend executes JavaScript to invoke the exposed binding:
    InspectorFrontendHost.loadNetworkResource('file:///etc/passwd', '', streamId);
    
  6. The browser-side scheme guard fails to detect this as a remote frontend, creates a FileURLLoaderFactory, and returns the local file content to the frontend context.

Suggested Fix

Update the detection of remote frontends to verify if the URL corresponds to a proxied remote path under the devtools scheme. For example:

bool is_remote_frontend =
    frontend_url.is_valid() && !frontend_url.IsAboutBlank() &&
    frontend_url.SchemeIs(content::kChromeDevToolsScheme) &&
    base::StartsWith(frontend_url.GetPath(), "/remote/");

Additionally, the unit tests in devtools_ui_bindings_unittest.cc should be modified to simulate realistic devtools://devtools/remote/... URLs.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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