Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Workers
DescriptionIncorrect authorization in Workers
ComponentWorkers
Bug ClassLogic Error
Tracker539032888
Fix commita14280eb93e5 (chromium/src) +271/-16
CISA KEVNot listed
CreditedAnonymous
Disclosed2026-08-20

Changed Functions

FunctionChangeNotes
if
content/browser/worker_host/dedicated_worker_host.cc
modified
TEST_F
content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
modified

Files Changed

  • content/browser/worker_host/dedicated_worker_host.cc
  • content/browser/worker_host/dedicated_worker_host.h
  • content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
From a14280eb93e574171c55abe6e7c4470a1b221f02 Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <[email protected]>
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 <[email protected]>
Auto-Submit: Yoshisato Yanagisawa <[email protected]>
Commit-Queue: Yoshisato Yanagisawa <[email protected]>
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();
+
Loading diff…

Regression Test / PoC

shipped with the fix
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",
Loading diff…

Original Bug Report

reported by [email protected]

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.

  1. Download all files then run : chmod +x poc.sh && ./poc.sh

  2. 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 AutoOpenFileTypes policy); the attacker does not set it during the episode.

  3. 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

  4. A normal HTTP page with no user activation starts one attachment download. Chrome saves downloaded-page.html and 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.

  5. 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 \

View on issue tracker