CVE-2026-76019
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/worker_host/dedicated_worker_host.cc |
modified | |
TEST_Fcontent/browser/worker_host/dedicated_worker_service_impl_unittest.cc |
modified |
Files Changed
content/browser/worker_host/dedicated_worker_host.cccontent/browser/worker_host/dedicated_worker_host.hcontent/browser/worker_host/dedicated_worker_service_impl_unittest.cc
Patch
From a14280eb93e574171c55abe6e7c4470a1b221f02 Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <yyanagisawa@chromium.org>
Date: Tue, 04 Aug 2026 02:39:05 -0700
Subject: [PATCH] Consolidate and pass file_url_support to WorkerScriptFetcher
This CL cleans up and consolidates the file URL support handling for
dedicated and shared worker script and subresource loading:
1. Introduce DoesCreatorAllowFileUrlSupport helper in worker_util to
centralize file URL support checks across creator contexts (handling
command line switches, WebPreferences, and nested worker inheritance).
2. Update WorkerScriptFetcher::CreateAndStart to take file_url_support
directly from callers (DedicatedWorkerHost and
SharedWorkerServiceImpl), removing redundant recalculations.
3. Add unit tests in WorkerUtilTest and DedicatedWorkerServiceImplTest
to verify file URL support behavior.
TAG=agy
CONV=6a5aeada-8c83-4b37-8615-53f25113df32
Bug: 539032888
Change-Id: I403e96584b67fb6f0a240ac4ef51de7577c4c793
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8182027
Reviewed-by: Hiroki Nakagawa <nhiroki@chromium.org>
Auto-Submit: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Commit-Queue: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1673207}
---
diff --git a/content/browser/worker_host/dedicated_worker_host.cc b/content/browser/worker_host/dedicated_worker_host.cc
index c4150dd8..8e448c3 100644
--- a/content/browser/worker_host/dedicated_worker_host.cc
+++ b/content/browser/worker_host/dedicated_worker_host.cc
@@ -10,6 +10,7 @@
#include <utility>
#include <variant>
+#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/safety_checks.h"
@@ -39,6 +40,7 @@
#include "content/browser/worker_host/dedicated_worker_hosts_for_document.h"
#include "content/browser/worker_host/dedicated_worker_service_impl.h"
#include "content/browser/worker_host/worker_script_fetcher.h"
+#include "content/browser/worker_host/worker_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/network_service_util.h"
@@ -48,6 +50,7 @@
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/common/child_process_id_util.h"
#include "content/public/common/content_client.h"
+#include "content/public/common/content_switches.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "mojo/public/cpp/system/message_pipe.h"
#include "net/base/isolation_info.h"
@@ -64,6 +67,7 @@
#include "third_party/blink/public/common/service_worker/service_worker_scope_match.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/common/tokens/tokens.h"
+#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "third_party/blink/public/mojom/back_forward_cache_not_restored_reasons.mojom.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "third_party/blink/public/mojom/loader/fetch_client_settings_object.mojom.h"
@@ -360,7 +364,13 @@
// initiator origin to keep consistency with WorkerScriptFetcher, but probably
// this should be calculated based on the worker origin as the factories be
// used for subresource loading on the worker.
- file_url_support_ = creator_origin_.scheme() == url::kFileScheme;
+ std::optional<blink::web_pref::WebPreferences> web_preferences;
+ if (creator_render_frame_host) {
+ web_preferences = creator_render_frame_host->GetOrCreateWebPreferences();
+ }
+ file_url_support_ = DoesCreatorAllowFileUrlSupport(
+ creator_origin_, web_preferences ? &*web_preferences : nullptr,
+ creator_worker ? creator_worker->file_url_support() : false);
// For blob URL workers, inherit the controller from the worker's parent.
// See https://w3c.github.io/ServiceWorker/#control-and-use-worker-client
@@ -409,7 +419,7 @@
nearest_ancestor_render_frame_host->GetIsolationInfoForSubresources(),
std::move(client_security_state), credentials_mode,
std::move(outside_fetch_client_settings_object),
- network::mojom::RequestDestination::kWorker,
+ network::mojom::RequestDestination::kWorker, file_url_support_,
storage_partition_impl->GetServiceWorkerContext(),
service_worker_handle_.get(), std::move(blob_url_loader_factory), nullptr,
storage_partition_impl, partition_domain,
diff --git a/content/browser/worker_host/dedicated_worker_host.h b/content/browser/worker_host/dedicated_worker_host.h
index 87227f7..47ce6834 100644
--- a/content/browser/worker_host/dedicated_worker_host.h
+++ b/content/browser/worker_host/dedicated_worker_host.h
@@ -246,6 +246,8 @@
return service_worker_handle_.get();
}
+ bool file_url_support() const { return file_url_support_; }
+
#if BUILDFLAG(ENABLE_COMPUTE_PRESSURE)
PressureServiceForDedicatedWorker* pressure_service() {
return pressure_service_.get();
diff --git a/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc b/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
index 7249859..25f6312 100644
--- a/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
+++ b/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
@@ -7,9 +7,11 @@
#include <memory>
#include <utility>
+#include "base/command_line.h"
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
+#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "components/services/storage/privileged/cpp/bucket_client_info.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
@@ -21,6 +23,7 @@
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/content_features.h"
+#include "content/public/common/content_switches.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_utils.h"
@@ -36,6 +39,7 @@
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/common/tokens/tokens.h"
#include "third_party/blink/public/common/tokens/tokens_mojom_traits.h"
+#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "third_party/blink/public/mojom/loader/fetch_client_settings_object.mojom.h"
#include "third_party/blink/public/mojom/service_worker/controller_service_worker.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_container.mojom.h"
@@ -52,21 +56,28 @@
public:
MockDedicatedWorker(ChildProcessId worker_process_id,
GlobalRenderFrameHostId render_frame_host_id,
+ const url::Origin& origin)
+ : MockDedicatedWorker(worker_process_id,
+ render_frame_host_id,
+ DedicatedWorkerCreator(render_frame_host_id),
+ origin) {}
+
+ MockDedicatedWorker(ChildProcessId worker_process_id,
+ GlobalRenderFrameHostId ancestor_render_frame_host_id,
+ DedicatedWorkerCreator creator,
const url::Origin& origin) {
// The COEP reporter is replaced by a placeholder connection. Reports are
// ignored.
+ auto* ancestor_rfh =
+ RenderFrameHostImpl::FromID(ancestor_render_frame_host_id);
auto coep_reporter = std::make_unique<CrossOriginEmbedderPolicyReporter>(
- RenderFrameHostImpl::FromID(render_frame_host_id)
- ->GetStoragePartition()
- ->GetWeakPtr(),
- GURL(), std::nullopt, std::nullopt, base::UnguessableToken::Create(),
+ ancestor_rfh->GetStoragePartition()->GetWeakPtr(), GURL(), std::nullopt,
+ std::nullopt, base::UnguessableToken::Create(),
net::NetworkAnonymizationKey());
DedicatedWorkerHostFactoryImpl::Create(
- *RenderFrameHostImpl::FromID(render_frame_host_id),
- factory_.BindNewPipeAndPassReceiver(), worker_process_id,
- /*creator=*/render_frame_host_id,
- RenderFrameHostImpl::FromID(render_frame_host_id)->GetWeakDocumentPtr(),
+ *ancestor_rfh, factory_.BindNewPipeAndPassReceiver(), worker_process_id,
+ creator, ancestor_rfh->GetWeakDocumentPtr(),
blink::StorageKey::CreateFirstParty(origin),
net::IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
network::mojom::ClientSecurityState::New(), PolicyContainerPolicies(),
@@ -165,6 +176,12 @@
->GetDedicatedWorkerService();
}
+ DedicatedWorkerServiceImpl* GetDedicatedWorkerServiceImpl() const {
+ return static_cast<DedicatedWorkerServiceImpl*>(
+ browser_context_->GetDefaultStoragePartition()
+ ->GetDedicatedWorkerService());
+ }
+
private:
std::unique_ptr<TestBrowserContext> browser_context_;
};
@@ -295,6 +312,118 @@
EXPECT_TRUE(observer.dedicated_worker_infos().empty());
}
+TEST_F(DedicatedWorkerServiceImplTest, FileUrlSupportInheritance) {
+ TestDedicatedWorkerServiceObserver observer;
+ base::ScopedObservation<DedicatedWorkerService,
+ DedicatedWorkerService::Observer>
+ scoped_observation(&observer);
+ scoped_observation.Observe(GetDedicatedWorkerService());
+
+ const GURL kFileUrl("file:///path/to/page.html");
+ const auto origin = url::Origin::Create(kFileUrl);
+ std::unique_ptr<TestWebContents> web_contents = CreateWebContents(kFileUrl);
+ TestRenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+
Regression Test / PoC
diff --git a/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc b/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
index 7249859..25f6312 100644
--- a/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
+++ b/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
@@ -7,9 +7,11 @@
#include <memory>
#include <utility>
+#include "base/command_line.h"
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
+#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "components/services/storage/privileged/cpp/bucket_client_info.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
@@ -21,6 +23,7 @@
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/content_features.h"
+#include "content/public/common/content_switches.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_utils.h"
@@ -36,6 +39,7 @@
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/common/tokens/tokens.h"
#include "third_party/blink/public/common/tokens/tokens_mojom_traits.h"
+#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "third_party/blink/public/mojom/loader/fetch_client_settings_object.mojom.h"
#include "third_party/blink/public/mojom/service_worker/controller_service_worker.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_container.mojom.h"
@@ -52,21 +56,28 @@
public:
MockDedicatedWorker(ChildProcessId worker_process_id,
GlobalRenderFrameHostId render_frame_host_id,
+ const url::Origin& origin)
+ : MockDedicatedWorker(worker_process_id,
+ render_frame_host_id,
+ DedicatedWorkerCreator(render_frame_host_id),
+ origin) {}
+
+ MockDedicatedWorker(ChildProcessId worker_process_id,
+ GlobalRenderFrameHostId ancestor_render_frame_host_id,
+ DedicatedWorkerCreator creator,
const url::Origin& origin) {
// The COEP reporter is replaced by a placeholder connection. Reports are
// ignored.
+ auto* ancestor_rfh =
+ RenderFrameHostImpl::FromID(ancestor_render_frame_host_id);
auto coep_reporter = std::make_unique<CrossOriginEmbedderPolicyReporter>(
- RenderFrameHostImpl::FromID(render_frame_host_id)
- ->GetStoragePartition()
- ->GetWeakPtr(),
- GURL(), std::nullopt, std::nullopt, base::UnguessableToken::Create(),
+ ancestor_rfh->GetStoragePartition()->GetWeakPtr(), GURL(), std::nullopt,
+ std::nullopt, base::UnguessableToken::Create(),
net::NetworkAnonymizationKey());
DedicatedWorkerHostFactoryImpl::Create(
- *RenderFrameHostImpl::FromID(render_frame_host_id),
- factory_.BindNewPipeAndPassReceiver(), worker_process_id,
- /*creator=*/render_frame_host_id,
- RenderFrameHostImpl::FromID(render_frame_host_id)->GetWeakDocumentPtr(),
+ *ancestor_rfh, factory_.BindNewPipeAndPassReceiver(), worker_process_id,
+ creator, ancestor_rfh->GetWeakDocumentPtr(),
blink::StorageKey::CreateFirstParty(origin),
net::IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
network::mojom::ClientSecurityState::New(), PolicyContainerPolicies(),
@@ -165,6 +176,12 @@
->GetDedicatedWorkerService();
}
+ DedicatedWorkerServiceImpl* GetDedicatedWorkerServiceImpl() const {
+ return static_cast<DedicatedWorkerServiceImpl*>(
+ browser_context_->GetDefaultStoragePartition()
+ ->GetDedicatedWorkerService());
+ }
+
private:
std::unique_ptr<TestBrowserContext> browser_context_;
};
@@ -295,6 +312,118 @@
EXPECT_TRUE(observer.dedicated_worker_infos().empty());
}
+TEST_F(DedicatedWorkerServiceImplTest, FileUrlSupportInheritance) {
+ TestDedicatedWorkerServiceObserver observer;
+ base::ScopedObservation<DedicatedWorkerService,
+ DedicatedWorkerService::Observer>
+ scoped_observation(&observer);
+ scoped_observation.Observe(GetDedicatedWorkerService());
+
+ const GURL kFileUrl("file:///path/to/page.html");
+ const auto origin = url::Origin::Create(kFileUrl);
+ std::unique_ptr<TestWebContents> web_contents = CreateWebContents(kFileUrl);
+ TestRenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+
+ // 1. Without switch or web preferences, file_url_support should be false.
+ {
+ auto mock_worker = std::make_unique<MockDedicatedWorker>(
+ rfh->GetProcess()->GetID(), rfh->GetGlobalId(), origin);
+ observer.RunUntilWorkerEvent();
+
+ ASSERT_EQ(observer.dedicated_worker_infos().size(), 1u);
+ blink::DedicatedWorkerToken token =
+ observer.dedicated_worker_infos().begin()->first;
+ DedicatedWorkerHost* host =
+ GetDedicatedWorkerServiceImpl()->GetDedicatedWorkerHostFromToken(token);
+ ASSERT_TRUE(host);
+ EXPECT_FALSE(host->file_url_support());
+
+ mock_worker = nullptr;
+ observer.RunUntilWorkerEvent();
+ EXPECT_TRUE(observer.dedicated_worker_infos().empty());
+ }
+
+ // 2. With WebPreferences::allow_file_access_from_file_urls, file_url_support
+ // should be true.
+ {
+ blink::web_pref::WebPreferences prefs =
+ web_contents->GetOrCreateWebPreferences();
+ prefs.allow_file_access_from_file_urls = true;
+ web_contents->SetWebPreferences(prefs);
+
+ auto mock_worker = std::make_unique<MockDedicatedWorker>(
+ rfh->GetProcess()->GetID(), rfh->GetGlobalId(), origin);
+ observer.RunUntilWorkerEvent();
+
+ ASSERT_EQ(observer.dedicated_worker_infos().size(), 1u);
+ blink::DedicatedWorkerToken token =
+ observer.dedicated_worker_infos().begin()->first;
+ DedicatedWorkerHost* host =
+ GetDedicatedWorkerServiceImpl()->GetDedicatedWorkerHostFromToken(token);
+ ASSERT_TRUE(host);
+ EXPECT_TRUE(host->file_url_support());
+
+ mock_worker = nullptr;
+ observer.RunUntilWorkerEvent();
+ EXPECT_TRUE(observer.dedicated_worker_infos().empty());
+
+ prefs.allow_file_access_from_file_urls = false;
+ web_contents->SetWebPreferences(prefs);
+ }
+
+ // 3. With WebPreferences::allow_universal_access_from_file_urls,
+ // file_url_support should be true.
+ {
+ blink::web_pref::WebPreferences prefs =
+ web_contents->GetOrCreateWebPreferences();
+ prefs.allow_universal_access_from_file_urls = true;
+ web_contents->SetWebPreferences(prefs);
+
+ auto mock_worker = std::make_unique<MockDedicatedWorker>(
+ rfh->GetProcess()->GetID(), rfh->GetGlobalId(), origin);
+ observer.RunUntilWorkerEvent();
+
+ ASSERT_EQ(observer.dedicated_worker_infos().size(), 1u);
+ blink::DedicatedWorkerToken token =
+ observer.dedicated_worker_infos().begin()->first;
+ DedicatedWorkerHost* host =
+ GetDedicatedWorkerServiceImpl()->GetDedicatedWorkerHostFromToken(token);
+ ASSERT_TRUE(host);
+ EXPECT_TRUE(host->file_url_support());
+
+ mock_worker = nullptr;
+ observer.RunUntilWorkerEvent();
+ EXPECT_TRUE(observer.dedicated_worker_infos().empty());
+
+ prefs.allow_universal_access_from_file_urls = false;
+ web_contents->SetWebPreferences(prefs);
+ }
+
+ // 4. With switches::kAllowFileAccessFromFiles, file_url_support should be
+ // true.
+ {
+ base::test::ScopedCommandLine scoped_command_line;
+ scoped_command_line.GetProcessCommandLine()->AppendSwitch(
+ switches::kAllowFileAccessFromFiles);
+
+ auto mock_worker = std::make_unique<MockDedicatedWorker>(
+ rfh->GetProcess()->GetID(), rfh->GetGlobalId(), origin);
+ observer.RunUntilWorkerEvent();
+
+ ASSERT_EQ(observer.dedicated_worker_infos().size(), 1u);
+ blink::DedicatedWorkerToken token =
+ observer.dedicated_worker_infos().begin()->first;
+ DedicatedWorkerHost* host =
+ GetDedicatedWorkerServiceImpl()->GetDedicatedWorkerHostFromToken(token);
+ ASSERT_TRUE(host);
+ EXPECT_TRUE(host->file_url_support());
+
+ mock_worker = nullptr;
+ observer.RunUntilWorkerEvent();
+ EXPECT_TRUE(observer.dedicated_worker_infos().empty());
+ }
+}
+
class DedicatedWorkerHostNavigationTest
: public DedicatedWorkerServiceImplTest {
public:
diff --git a/content/browser/worker_host/worker_util_unittest.cc b/content/browser/worker_host/worker_util_unittest.cc
new file mode 100644
index 0000000..e1de76cb
--- /dev/null
+++ b/content/browser/worker_host/worker_util_unittest.cc
@@ -0,0 +1,68 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/worker_host/worker_util.h"
+
+#include "base/command_line.h"
+#include "base/test/scoped_command_line.h"
+#include "content/public/common/content_switches.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/web_preferences/web_preferences.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace content {
+
+TEST(WorkerUtilTest, DoesCreatorAllowFileUrlSupport) {
+ const url::Origin kHttpOrigin =
+ url::Origin::Create(GURL("http://example.com"));
+ const url::Origin kFileOrigin =
+ url::Origin::Create(GURL("file:///path/to/page.html"));
+
+ blink::web_pref::WebPreferences default_prefs;
+ blink::web_pref::WebPreferences allow_file_prefs;
+ allow_file_prefs.allow_file_access_from_file_urls = true;
+ blink::web_pref::WebPreferences allow_universal_prefs;
+ allow_universal_prefs.allow_universal_access_from_file_urls = true;
+
+ // 1. Non-file origin should never allow file URL support.
+ EXPECT_FALSE(DoesCreatorAllowFileUrlSupport(kHttpOrigin, &default_prefs));
+ EXPECT_FALSE(DoesCreatorAllowFileUrlSupport(kHttpOrigin, &allow_file_prefs));
+ EXPECT_FALSE(DoesCreatorAllowFileUrlSupport(
+ kHttpOrigin, /*web_preferences=*/nullptr,
+ /*creator_worker_has_file_url_support=*/true));
+
+ // 2. File origin without flags/preferences or parent support should be false.
+ EXPECT_FALSE(DoesCreatorAllowFileUrlSupport(kFileOrigin, &default_prefs));
+ EXPECT_FALSE(DoesCreatorAllowFileUrlSupport(
+ kFileOrigin, /*web_preferences=*/nullptr,
+ /*creator_worker_has_file_url_support=*/false));
+
+ // 3. File origin with WebPreferences::allow_file_access_from_file_urls.
+ EXPECT_TRUE(DoesCreatorAllowFileUrlSupport(kFileOrigin, &allow_file_prefs));
+
+ // 4. File origin with WebPreferences::allow_universal_access_from_file_urls.
+ EXPECT_TRUE(
+ DoesCreatorAllowFileUrlSupport(kFileOrigin, &allow_universal_prefs));
+
+ // 5. File origin inheriting from creator worker with file URL support.
+ EXPECT_TRUE(DoesCreatorAllowFileUrlSupport(
+ kFileOrigin, /*web_preferences=*/nullptr,
+ /*creator_worker_has_file_url_support=*/true));
+
+ // 6. File origin with switches::kAllowFileAccessFromFiles.
+ {
+ base::test::ScopedCommandLine scoped_command_line;
+ scoped_command_line.GetProcessCommandLine()->AppendSwitch(
+ switches::kAllowFileAccessFromFiles);
+ EXPECT_TRUE(DoesCreatorAllowFileUrlSupport(kFileOrigin, &default_prefs));
+ EXPECT_TRUE(DoesCreatorAllowFileUrlSupport(
+ kFileOrigin, /*web_preferences=*/nullptr,
+ /*creator_worker_has_file_url_support=*/false));
+ // Even with the switch, non-file origins should still be rejected.
+ EXPECT_FALSE(DoesCreatorAllowFileUrlSupport(kHttpOrigin, &default_prefs));
+ }
+}
+
+} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 7455e883..b79615d 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2998,6 +2998,7 @@
"../browser/worker_host/shared_worker_service_impl_unittest.cc",
"../browser/worker_host/worker_script_fetcher_unittest.cc",
"../browser/worker_host/worker_script_loader_factory_unittest.cc",
+ "../browser/worker_host/worker_util_unittest.cc",
"../browser/xr/metrics/session_tracker_unittest.cc",
"../child/blink_platform_impl_unittest.cc",
"../child/child_performance_coordinator_unittest.cc",
Original Bug Report
WorkerScriptFetcher allows compromised file renderer to bypass Chrome local-file policy
Steps to reproduce the problem
Tested on macOS 26.5.2 with official Google Chrome Stable 150.0.7871.187 from Google’s Stable DMG. The individually attached files are
self-contained and do not require a Chromium checkout, build output, archive extraction, or pre-existing web server. receiver.js is a readable JavaScript bundle containing the exact generated Mojo dependency closure.
-
Download all files then run :
chmod +x poc.sh && ./poc.sh -
The launcher starts its own loopback web server. It creates a disposable positive profile with HTML already configured to open automatically. This represents the supported prior “Always open files of this type” choice (or
AutoOpenFileTypespolicy); the attacker does not set it during the episode. -
The default target is
~/.bash_history, then~/.zsh_history, then/etc/hosts. A different readable file can be selected with./poc.sh --target /absolute/path/to/your/desired/file -
A normal HTTP page with no user activation starts one attachment download. Chrome saves
downloaded-page.htmland opens that exact file automatically in a new Site-Isolated renderer. The destination page explains that the download is only the bootstrap, then displays the unrelated target file’s cleartext contents. -
Run the positive and fresh-profile differential automatically with
./poc.sh --both --headless --target /absolute/path/to/your/desired/file
Note 1: An exact non-default installation can be selected with --chrome /path/to/Google\ Chrome.
Note 2: this PoC uses MojoJS to simulate native compromise of the auto-opened file: renderer. The bug does not itself provide renderer code execution.
Problem Description
Chromium’s default allow_file_access_from_file_urls == false policy prevents a file: document from reading local URLs. Blink records this restriction in the document’s SecurityOrigin. Due to this behavior, normal fetch(target) fails in this following way: new Worker(target) throws SecurityError, and Chromium’s macOS sandbox profile denies the destination renderer direct access to the same path.
A committed file document nevertheless receives renderer-facing browser interfaces, including a generic FileURLLoaderFactory and a document-scoped DedicatedWorkerHostFactory interfaces. A compromised renderer can supply an attacker-chosen file: worker-script URL through the production worker interface.
The browser authenticates the requesting renderer but in the process it loses Blink’s local-access restriction; WorkerScriptFetcher sees only that the creator scheme is file and installs a generic browser-side FileURLLoaderFactory. That factory treats the file:// initiator and target as the same tuple origin, converts the URL to a native path, and applies ChromeNetworkDelegate::IsAccessAllowed(). On macOS (possibly on Linux, haven;t checked) that desktop policy accepts every path, leaving only the browser process’s OS access.
Chrome therefore it opens the target with browser privileges and returns a kBasic response body data pipe. DedicatedWorkerHost::DidStartScriptLoad() forwards it to the renderer-controlled client, which drains the bytes before Blink parses or
executes them as a worker script. The renderer sandbox and Blink’s normal denial are bypassed because the privileged read and body delivery already occurred in the browser process. MojoJS exposes production IPC; Site Isolation remains enabled, thus this assumes a separate compromise of the auto-opened file renderer rather than migration of the HTTP renderer compromise.
The effect is disclosing the content of attacker-known file paths readable by Chrome’s OS account only, without path enumeration, file write nor code execution. Prior HTML auto-open state is a requirement for the no-interaction web entry and is not present in a fresh profile; auto-open itself is not alleged to be the vulnerability.
Additional Comments
Save all attached files in the same directory. receiver.js bundles the exact generated Mojo dependency closure. Attached is a video of this bug for further reference.
Summary
WorkerScriptFetcher allows compromised file renderer to bypass Chrome local-file policy
Custom Questions
Type of crash:
Non applicable
Crash state:
Non applicable
Reporter credit:
Anonymous
Additional Data
Category: Security
Chrome Channel: Stable
Regression: N/A \