High watch CVSS 8.1 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Workers
DescriptionIncorrect authorization in Workers
ComponentWorkers
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker539032888
Fix commita14280eb93e5 (chromium/src) +271/-16
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N
CISA KEVNot listed
CreditedAnonymous
Reported2026-07-26 (per vendor advisory)
Fix landed2026-08-04 (public git)
Disclosed2026-08-20
Patch gap16 days — fix public in source before the advisory

Background

`file_url_support`
a browser-side flag that decides whether the URL loader factory bundles built by CreateFactoryBundle for a worker include a file: scheme loader, i.e. whether the worker’s script fetch and its subresource requests can load local file:// URLs.
File-access WebPreferences
allow_file_access_from_file_urls and allow_universal_access_from_file_urls are off by default and are normally enabled only by embedders or tests. The --allow-file-access-from-files switch (switches::kAllowFileAccessFromFiles) is the command-line equivalent that lets file:// documents read other local files.
`WorkerScriptFetcher::CreateAndStart`
the browser-process entry point that builds the main-resource and subresource loader factory bundles for dedicated and shared workers and starts the top-level script fetch.
Creator context
the document (creator_render_frame_host) or parent worker (creator_worker) that constructs a worker; a nested dedicated worker has a worker, not a frame, as its creator.

Root Cause Analysis

Before the patch, file-URL loading for workers was granted on the scheme alone. DedicatedWorkerHost set file_url_support_ = creator_origin_.scheme() == url::kFileScheme. WorkerScriptFetcher::CreateAndStart separately computed constructor_uses_file_url = request_initiator.scheme() == url::kFileScheme and passed it to CreateFactoryBundle for both the main-resource and subresource bundles. Any worker created by a file:// page therefore got a loader factory able to fetch arbitrary local file:// URLs. This held even when the user and embedder had not enabled allow_file_access_from_file_urls, allow_universal_access_from_file_urls or --allow-file-access-from-files, the settings that control file-to-file access for documents.

The patch tightens the check. The new DoesCreatorAllowFileUrlSupport() returns false for non-file creators. For file creators it returns true only if the command-line switch is present or the creator’s WebPreferences grant file or universal access. When no frame preferences are available (a nested worker), it inherits the parent worker’s file_url_support(). Both DedicatedWorkerHost and SharedWorkerServiceImpl now compute the value this way and pass it explicitly into CreateAndStart, which no longer derives it from request_initiator. The new unit test confirms the intended rule: a file:// creator with no switch or preferences now produces file_url_support() == false.

Key insight
The worker host treated “the creator is a file:// page” as permission to load file URLs, which skipped the opt-in file-access preferences that govern file:// documents. The fix makes the authorization depend on those preferences or the switch, and on inheritance from a parent worker, and passes one consistent decision to the loader-factory construction.

Attack Path

  1. Local HTML opened The victim opens an attacker-supplied HTML file from disk through a file:// URL, for example a downloaded attachment. Default settings do not grant this page file-to-file access.
  2. Worker construction The page creates a dedicated worker (or a shared worker), so DedicatedWorkerHost or SharedWorkerServiceImpl calls WorkerScriptFetcher::CreateAndStart with a file-scheme creator origin.
  3. Over-privileged factory Before the fix, the scheme check alone set file URL support, so CreateFactoryBundle gave the worker main-resource and subresource factories that can load file:// URLs.
  4. Local file access from the worker The worker could then request other local files, which the document itself could not do under default preferences. Whether the response bodies could be read and sent off-device is (inferred); the patch shows only the authorization of the loader factory.

Impact Assessment

An attacker who gets a victim to open a crafted local HTML file could use a worker to get file:// loading rights that default settings deny to file:// documents. This could expose local files to the attacker’s script (inferred). The bug sits in browser-process authorization of the worker’s URL loader factories. It needs a user to open a local file, so ordinary web content on http(s) origins cannot reach it, because the new helper returns false for non-file creators and the old checks also required the file scheme. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.1.

Changed Functions

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

Files Changed

  • content/browser/worker_host/worker_util.cc
  • content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
  • content/browser/worker_host/dedicated_worker_host.h
  • content/browser/worker_host/worker_util.h
  • content/browser/worker_host/worker_util_unittest.cc
  • content/browser/worker_host/worker_script_fetcher.h
  • content/browser/worker_host/shared_worker_service_impl.cc
  • content/test/BUILD.gn
  • content/browser/worker_host/dedicated_worker_host.cc
  • content/browser/worker_host/worker_script_fetcher.cc

Audit Directions

  • Scheme used as authorization
    Search browser-side loader setup (CreateFactoryBundle, navigation and worker factory creation, NonNetworkURLLoaderFactory registration) for scheme() == url::kFileScheme checks that grant file: factories without also consulting allow_file_access_from_file_urls or kAllowFileAccessFromFiles.
  • Worker and worklet parity with documents
    Compare the capabilities that service workers, worklets, nested workers and blob-URL workers get against those of their creator document. Look especially at cases where a value is recomputed from request_initiator instead of being inherited from the creator’s already-authorized state.
  • Duplicated security decisions
    Wherever the same permission is computed in two places, as with DedicatedWorkerHost and WorkerScriptFetcher here, check that both copies agree and that the more permissive one is not the one that controls the actual factory.
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/shared_worker_service_impl.cc b/content/browser/worker_host/shared_worker_service_impl.cc
index 19224df9..9973fa8 100644
--- a/content/browser/worker_host/shared_worker_service_impl.cc
+++ b/content/browser/worker_host/shared_worker_service_impl.cc
@@ -11,6 +11,7 @@
 #include <string>
 
 #include "base/check_op.h"
+#include "base/command_line.h"
 #include "base/feature_list.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
@@ -48,6 +49,7 @@
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/common/messaging/message_port_channel.h"
 #include "third_party/blink/public/common/storage_key/storage_key.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/script/script_type.mojom.h"
 #include "third_party/blink/public/mojom/worker/shared_worker_client.mojom.h"
@@ -521,6 +523,12 @@
       << host->instance().creator_storage_key().origin()
       << " should be the same.";
 
+  const blink::web_pref::WebPreferences& prefs =
+      creator.GetOrCreateWebPreferences();
+  bool file_url_support = DoesCreatorAllowFileUrlSupport(
+      host->instance().creator_storage_key().origin(), &prefs,
+      /*creator_worker_has_file_url_support=*/false);
+
   WorkerScriptFetcher::CreateAndStart(
       worker_process_host->GetDeprecatedID(), host->token(),
       host->instance().url(), creator, &creator, /*creator_worker=*/nullptr,
@@ -532,7 +540,7 @@
       host->instance().worker_storage_key().ToPartialNetIsolationInfo(),
       creator.BuildClientSecurityStateForWorkers(), credentials_mode,
       std::move(outside_fetch_client_settings_object),
-      network::mojom::RequestDestination::kSharedWorker,
+      network::mojom::RequestDestination::kSharedWorker, file_url_support,
       service_worker_context_, service_worker_handle_raw,
       std::move(blob_url_loader_factory), url_loader_factory_override_,
       storage_partition_, storage_domain,
diff --git a/content/browser/worker_host/worker_script_fetcher.cc b/content/browser/worker_host/worker_script_fetcher.cc
index f49da48..6f67f02 100644
--- a/content/browser/worker_host/worker_script_fetcher.cc
+++ b/content/browser/worker_host/worker_script_fetcher.cc
@@ -253,6 +253,7 @@
     blink::mojom::FetchClientSettingsObjectPtr
         outside_fetch_client_settings_object,
     network::mojom::RequestDestination request_destination,
+    bool file_url_support,
     scoped_refptr<ServiceWorkerContextWrapper> service_worker_context,
     ServiceWorkerMainResourceHandle* service_worker_handle,
     scoped_refptr<network::SharedURLLoaderFactory> blob_url_loader_factory,
@@ -282,9 +283,6 @@
     return;
   }
 
-  bool constructor_uses_file_url =
-      request_initiator.scheme() == url::kFileScheme;
-
   // TODO(crbug.com/41472712): Filesystem URL support on shared workers
   // are now broken.
   bool filesystem_url_support =
@@ -297,13 +295,13 @@
   std::unique_ptr<blink::PendingURLLoaderFactoryBundle>
       factory_bundle_for_browser = CreateFactoryBundle(
           LoaderType::kMainResource, worker_process_id, storage_partition,
-          storage_domain, constructor_uses_file_url, filesystem_url_support,
+          storage_domain, file_url_support, filesystem_url_support,
           creator_render_frame_host, request_initiator_storage_key,
           request_destination);
   std::unique_ptr<blink::PendingURLLoaderFactoryBundle>
       subresource_loader_factories = CreateFactoryBundle(
           LoaderType::kSubResource, worker_process_id, storage_partition,
-          storage_domain, constructor_uses_file_url, filesystem_url_support,
+          storage_domain, file_url_support, filesystem_url_support,
           creator_render_frame_host, request_initiator_storage_key,
           request_destination);
 
diff --git a/content/browser/worker_host/worker_script_fetcher.h b/content/browser/worker_host/worker_script_fetcher.h
index 9933f19..0c20572 100644
--- a/content/browser/worker_host/worker_script_fetcher.h
+++ b/content/browser/worker_host/worker_script_fetcher.h
@@ -137,6 +137,7 @@
       blink::mojom::FetchClientSettingsObjectPtr
           outside_fetch_client_settings_object,
       network::mojom::RequestDestination request_destination,
+      bool file_url_support,
       scoped_refptr<ServiceWorkerContextWrapper> service_worker_context,
       ServiceWorkerMainResourceHandle* service_worker_handle,
       scoped_refptr<network::SharedURLLoaderFactory> blob_url_loader_factory,
diff --git a/content/browser/worker_host/worker_util.cc b/content/browser/worker_host/worker_util.cc
index 4cd7ae7..2ee4e868 100644
--- a/content/browser/worker_host/worker_util.cc
+++ b/content/browser/worker_host/worker_util.cc
@@ -4,9 +4,12 @@
 
 #include "content/browser/worker_host/worker_util.h"
 
+#include "base/command_line.h"
 #include "base/feature_list.h"
+#include "content/public/common/content_switches.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
+#include "third_party/blink/public/common/web_preferences/web_preferences.h"
 #include "third_party/blink/public/mojom/storage_key/ancestor_chain_bit.mojom.h"
 #include "url/gurl.h"
 #include "url/origin.h"
@@ -42,4 +45,22 @@
   return worker_storage_key.origin();
 }
 
+bool DoesCreatorAllowFileUrlSupport(
+    const url::Origin& creator_origin,
+    const blink::web_pref::WebPreferences* web_preferences,
+    bool creator_worker_has_file_url_support) {
+  if (creator_origin.scheme() != url::kFileScheme) {
+    return false;
+  }
+  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
+          switches::kAllowFileAccessFromFiles)) {
+    return true;
+  }
+  if (web_preferences) {
+    return web_preferences->allow_file_access_from_file_urls ||
+           web_preferences->allow_universal_access_from_file_urls;
+  }
+  return creator_worker_has_file_url_support;
+}
+
 }  // namespace content
diff --git a/content/browser/worker_host/worker_util.h b/content/browser/worker_host/worker_util.h
index 6e37519..d62f3d1 100644
--- a/content/browser/worker_host/worker_util.h
+++ b/content/browser/worker_host/worker_util.h
@@ -7,6 +7,11 @@
 #include "content/common/content_export.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
 #include "url/gurl.h"
+#include "url/origin.h"
+
+namespace blink::web_pref {
+struct WebPreferences;
+}  // namespace blink::web_pref
 
 namespace content {
 
@@ -39,6 +44,18 @@
     const blink::StorageKey& worker_storage_key,
     bool is_opaque_origin_enabled);
 
+// Returns whether the worker should be allowed to load file URLs.
+// This is allowed only when `creator_origin` is a file URL and at least one of
+// the following conditions is met:
+// - The --allow-file-access-from-files command-line switch is present.
+// - `web_preferences` allows file access from file URLs or universal access
+//   from file URLs.
+// - `creator_worker_has_file_url_support` is true.
+CONTENT_EXPORT bool DoesCreatorAllowFileUrlSupport(
+    const url::Origin& creator_origin,
+    const blink::web_pref::WebPreferences* web_preferences,
+    bool creator_worker_has_file_url_support = false);
+
 }  // namespace content
 
 #endif  // CONTENT_BROWSER_WORKER_HOST_WORKER_UTIL_H_
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;
... (file diff truncated)
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 fi...@gmail.com

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