Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Loader
DescriptionIncorrect authorization in Loader
ComponentLoader
Bug ClassLogic Error
Tracker513346220
Fix commit03b026a35f12 (chromium/src) +114/-11
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
WorkerFetchContext
third_party/blink/renderer/core/loader/worker_fetch_context.h
modified
CORE_EXPORT
third_party/blink/renderer/core/loader/worker_fetch_context.h
modified
TEST_F
third_party/blink/renderer/core/workers/dedicated_worker_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/loader/worker_fetch_context.cc
  • third_party/blink/renderer/core/loader/worker_fetch_context.h
  • third_party/blink/renderer/core/workers/dedicated_worker_test.cc
From 03b026a35f127ea477f52e3ab645831107277624 Mon Sep 17 00:00:00 2001
From: Minoru Chikamune <chikamune@chromium.org>
Date: Fri, 31 Jul 2026 06:39:35 -0700
Subject: [PATCH] Require same-origin for credentialed worker subresources

WorkerFetchContext::ShouldBlockFetchAsCredentialedSubresource allowed a subresource request with embedded credentials when the credentials matched those of the worker's own URL, without also requiring the target to be same-origin with the worker.

Align this with FrameFetchContext by additionally requiring the target URL's origin to match the fetch client settings object's security origin before applying the matching-credentials exemption.

Adds a unit test that constructs a WorkerFetchContext with a credentialed worker URL and verifies that a same-origin credentialed subresource is allowed while a cross-origin one is blocked.

TAG=agy
CONV=aa87476d-f281-4f69-a387-9245d3f91b52

Fixed: 513346220
Change-Id: I7f7c75305d7b2aaca1219ab3e88665ebbd105cda
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8170921
Reviewed-by: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Reviewed-by: Shunya Shishido <sisidovski@chromium.org>
Auto-Submit: Minoru Chikamune <chikamune@chromium.org>
Commit-Queue: Minoru Chikamune <chikamune@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1671796}
---

diff --git a/third_party/blink/renderer/core/loader/worker_fetch_context.cc b/third_party/blink/renderer/core/loader/worker_fetch_context.cc
index 824d290..e4e322e 100644
--- a/third_party/blink/renderer/core/loader/worker_fetch_context.cc
+++ b/third_party/blink/renderer/core/loader/worker_fetch_context.cc
@@ -29,6 +29,7 @@
 #include "third_party/blink/renderer/platform/runtime_enabled_features.h"
 #include "third_party/blink/renderer/platform/scheduler/public/virtual_time_controller.h"
 #include "third_party/blink/renderer/platform/supplementable.h"
+#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
 #include "third_party/blink/renderer/platform/weborigin/security_policy.h"
 
 namespace blink {
@@ -158,17 +159,29 @@
 bool WorkerFetchContext::ShouldBlockFetchAsCredentialedSubresource(
     const ResourceRequest& resource_request,
     const KURL& url) const {
-  if ((!url.User().empty() || !url.Pass().empty()) &&
-      resource_request.GetRequestContext() !=
-          mojom::blink::RequestContextType::XML_HTTP_REQUEST) {
-    if (Url().User() != url.User() || Url().Pass() != url.Pass()) {
-      CountDeprecation(
-          WebFeature::kRequestedSubresourceWithEmbeddedCredentials);
-
-      return true;
-    }
+  // URLs with no embedded credentials should load correctly.
+  if (url.User().empty() && url.Pass().empty()) {
+    return false;
   }
-  return false;
+
+  if (resource_request.GetRequestContext() ==
+      mojom::blink::RequestContextType::XML_HTTP_REQUEST) {
+    return false;
+  }
+
+  // Relative URLs on worker scripts that were loaded with embedded credentials
+  // should load correctly if same-origin.
+  if (Url().User() == url.User() && Url().Pass() == url.Pass() &&
+      SecurityOrigin::Create(url)->IsSameOriginWith(
+          GetResourceFetcherProperties()
+              .GetFetchClientSettingsObject()
+              .GetSecurityOrigin())) {
+    return false;
+  }
+
+  CountDeprecation(WebFeature::kRequestedSubresourceWithEmbeddedCredentials);
+
+  return true;
 }
 
 const KURL& WorkerFetchContext::Url() const {
diff --git a/third_party/blink/renderer/core/loader/worker_fetch_context.h b/third_party/blink/renderer/core/loader/worker_fetch_context.h
index f77aaba..040c060 100644
--- a/third_party/blink/renderer/core/loader/worker_fetch_context.h
+++ b/third_party/blink/renderer/core/loader/worker_fetch_context.h
@@ -6,12 +6,14 @@
 #define THIRD_PARTY_BLINK_RENDERER_CORE_LOADER_WORKER_FETCH_CONTEXT_H_
 
 #include <memory>
+
 #include "base/task/single_thread_task_runner.h"
 #include "base/types/optional_ref.h"
 #include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink-forward.h"
 #include "third_party/blink/public/mojom/loader/content_security_notifier.mojom-blink.h"
 #include "third_party/blink/public/mojom/loader/request_context_frame_type.mojom-blink-forward.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_object.mojom-blink-forward.h"
+#include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/loader/base_fetch_context.h"
 #include "third_party/blink/renderer/platform/heap/cross_thread_persistent.h"
 #include "third_party/blink/renderer/platform/loader/fetch/resource_loader_options.h"
@@ -35,7 +37,7 @@
 // Separate WorkerFetchContext objects (and separate ResourceFetcher objects)
 // are used for each of insideSettings fetch and outsideSettings fetches.
 // For more details, see core/workers/README.md.
-class WorkerFetchContext final : public BaseFetchContext {
+class CORE_EXPORT WorkerFetchContext final : public BaseFetchContext {
  public:
   WorkerFetchContext(const DetachableResourceFetcherProperties&,
                      WorkerOrWorkletGlobalScope&,
diff --git a/third_party/blink/renderer/core/workers/dedicated_worker_test.cc b/third_party/blink/renderer/core/workers/dedicated_worker_test.cc
index aff854f..e4ae2b8 100644
--- a/third_party/blink/renderer/core/workers/dedicated_worker_test.cc
+++ b/third_party/blink/renderer/core/workers/dedicated_worker_test.cc
@@ -14,6 +14,7 @@
 #include "base/test/bind.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/security_context/insecure_request_policy.mojom-blink.h"
 #include "third_party/blink/public/mojom/v8_cache_options.mojom-blink.h"
 #include "third_party/blink/public/mojom/worker/dedicated_worker_host.mojom-blink.h"
 #include "third_party/blink/public/platform/task_type.h"
@@ -32,6 +33,9 @@
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/inspector/thread_debugger_common_impl.h"
+#include "third_party/blink/renderer/core/loader/empty_clients.h"
+#include "third_party/blink/renderer/core/loader/worker_fetch_context.h"
+#include "third_party/blink/renderer/core/loader/worker_resource_timing_notifier_impl.h"
 #include "third_party/blink/renderer/core/messaging/blink_transferable_message.h"
 #include "third_party/blink/renderer/core/messaging/message_channel.h"
 #include "third_party/blink/renderer/core/messaging/message_port.h"
@@ -52,6 +56,9 @@
 #include "third_party/blink/renderer/core/workers/worker_thread_test_helper.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
 #include "third_party/blink/renderer/platform/heap/visitor.h"
+#include "third_party/blink/renderer/platform/loader/fetch/fetch_client_settings_object_snapshot.h"
+#include "third_party/blink/renderer/platform/loader/fetch/resource_request.h"
+#include "third_party/blink/renderer/platform/loader/testing/test_resource_fetcher_properties.h"
 #include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h"
 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
@@ -951,6 +958,87 @@
   EXPECT_EQ(event->ports(), nullptr);
 }
 
+TEST_F(DedicatedWorkerTest, SubresourceWithEmbeddedCredentials) {
+  StartWorker();
+  WaitUntilWorkerIsRunning();
+
+  base::RunLoop run_loop;
+  PostCrossThreadTask(
+      *GetWorkerThread()->GetTaskRunner(TaskType::kInternalTest), FROM_HERE,
+      CrossThreadBindOnce(
+          [](DedicatedWorkerThreadForTest* worker_thread,
+             CrossThreadOnceClosure quit_closure) {
+            auto* global_scope =
+                To<WorkerGlobalScope>(worker_thread->GlobalScope());
+
+            // Set up a WorkerFetchContext whose worker URL carries embedded
+            // credentials.
+            const KURL worker_url("http://user:pass@a.test/worker.js");
+            scoped_refptr<const SecurityOrigin> origin =
+                SecurityOrigin::Create(worker_url);
+            auto* settings_object =
+                MakeGarbageCollected<FetchClientSettingsObjectSnapshot>(
+                    worker_url, worker_url, origin,
+                    mojom::blink::PolicyContainerPolicies::New(), String(),
+                    HttpsState::kNone, AllowedByNosniff::MimeTypeCheck::kStrict,
+                    mojom::blink::InsecureRequestPolicy::
+                        kLeaveInsecureRequestsAlone,
+                    FetchClientSettingsObject::InsecureNavigationsSet());
+            auto& properties =
+                MakeGarbageCollected<TestResourceFetcherProperties>(
+                    *settings_object)
+                    ->MakeDetachable();
+            auto* fetch_context = MakeGarbageCollected<WorkerFetchContext>(
+                properties, *global_scope,
+                base::MakeRefCounted<EmptyWebWorkerFetchContext>(),
+                /*subresource_filter=*/nullptr,
+                *global_scope->GetContentSecurityPolicy(),
+                *MakeGarbageCollected<NullWorkerResourceTimingNotifier>());
+
+            ResourceRequest script_request;
+            script_request.SetRequestContext(
+                mojom::blink::RequestContextType::SCRIPT);
+
+            // A same-origin URL with credentials matching the worker's URL
+            // should be allowed.
+            EXPECT_FALSE(
+                fetch_context->ShouldBlockFetchAsCredentialedSubresource(
+                    script_request, KURL("http://user:pass@a.test/script.js")));
+
+            // A same-origin URL with non-matching embedded credentials must be
+            // blocked.
+            EXPECT_TRUE(
+                fetch_context->ShouldBlockFetchAsCredentialedSubresource(
+                    script_request,
+                    KURL("http://wrong:pass@a.test/script.js")));
+
+            // A cross-origin URL must be blocked even when its credentials
+            // match the worker's URL.
+            EXPECT_TRUE(
+                fetch_context->ShouldBlockFetchAsCredentialedSubresource(
+                    script_request, KURL("http://user:pass@b.test/script.js")));
+
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/workers/dedicated_worker_test.cc b/third_party/blink/renderer/core/workers/dedicated_worker_test.cc
index aff854f..e4ae2b8 100644
--- a/third_party/blink/renderer/core/workers/dedicated_worker_test.cc
+++ b/third_party/blink/renderer/core/workers/dedicated_worker_test.cc
@@ -14,6 +14,7 @@
 #include "base/test/bind.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/security_context/insecure_request_policy.mojom-blink.h"
 #include "third_party/blink/public/mojom/v8_cache_options.mojom-blink.h"
 #include "third_party/blink/public/mojom/worker/dedicated_worker_host.mojom-blink.h"
 #include "third_party/blink/public/platform/task_type.h"
@@ -32,6 +33,9 @@
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/inspector/thread_debugger_common_impl.h"
+#include "third_party/blink/renderer/core/loader/empty_clients.h"
+#include "third_party/blink/renderer/core/loader/worker_fetch_context.h"
+#include "third_party/blink/renderer/core/loader/worker_resource_timing_notifier_impl.h"
 #include "third_party/blink/renderer/core/messaging/blink_transferable_message.h"
 #include "third_party/blink/renderer/core/messaging/message_channel.h"
 #include "third_party/blink/renderer/core/messaging/message_port.h"
@@ -52,6 +56,9 @@
 #include "third_party/blink/renderer/core/workers/worker_thread_test_helper.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
 #include "third_party/blink/renderer/platform/heap/visitor.h"
+#include "third_party/blink/renderer/platform/loader/fetch/fetch_client_settings_object_snapshot.h"
+#include "third_party/blink/renderer/platform/loader/fetch/resource_request.h"
+#include "third_party/blink/renderer/platform/loader/testing/test_resource_fetcher_properties.h"
 #include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h"
 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
@@ -951,6 +958,87 @@
   EXPECT_EQ(event->ports(), nullptr);
 }
 
+TEST_F(DedicatedWorkerTest, SubresourceWithEmbeddedCredentials) {
+  StartWorker();
+  WaitUntilWorkerIsRunning();
+
+  base::RunLoop run_loop;
+  PostCrossThreadTask(
+      *GetWorkerThread()->GetTaskRunner(TaskType::kInternalTest), FROM_HERE,
+      CrossThreadBindOnce(
+          [](DedicatedWorkerThreadForTest* worker_thread,
+             CrossThreadOnceClosure quit_closure) {
+            auto* global_scope =
+                To<WorkerGlobalScope>(worker_thread->GlobalScope());
+
+            // Set up a WorkerFetchContext whose worker URL carries embedded
+            // credentials.
+            const KURL worker_url("http://user:pass@a.test/worker.js");
+            scoped_refptr<const SecurityOrigin> origin =
+                SecurityOrigin::Create(worker_url);
+            auto* settings_object =
+                MakeGarbageCollected<FetchClientSettingsObjectSnapshot>(
+                    worker_url, worker_url, origin,
+                    mojom::blink::PolicyContainerPolicies::New(), String(),
+                    HttpsState::kNone, AllowedByNosniff::MimeTypeCheck::kStrict,
+                    mojom::blink::InsecureRequestPolicy::
+                        kLeaveInsecureRequestsAlone,
+                    FetchClientSettingsObject::InsecureNavigationsSet());
+            auto& properties =
+                MakeGarbageCollected<TestResourceFetcherProperties>(
+                    *settings_object)
+                    ->MakeDetachable();
+            auto* fetch_context = MakeGarbageCollected<WorkerFetchContext>(
+                properties, *global_scope,
+                base::MakeRefCounted<EmptyWebWorkerFetchContext>(),
+                /*subresource_filter=*/nullptr,
+                *global_scope->GetContentSecurityPolicy(),
+                *MakeGarbageCollected<NullWorkerResourceTimingNotifier>());
+
+            ResourceRequest script_request;
+            script_request.SetRequestContext(
+                mojom::blink::RequestContextType::SCRIPT);
+
+            // A same-origin URL with credentials matching the worker's URL
+            // should be allowed.
+            EXPECT_FALSE(
+                fetch_context->ShouldBlockFetchAsCredentialedSubresource(
+                    script_request, KURL("http://user:pass@a.test/script.js")));
+
+            // A same-origin URL with non-matching embedded credentials must be
+            // blocked.
+            EXPECT_TRUE(
+                fetch_context->ShouldBlockFetchAsCredentialedSubresource(
+                    script_request,
+                    KURL("http://wrong:pass@a.test/script.js")));
+
+            // A cross-origin URL must be blocked even when its credentials
+            // match the worker's URL.
+            EXPECT_TRUE(
+                fetch_context->ShouldBlockFetchAsCredentialedSubresource(
+                    script_request, KURL("http://user:pass@b.test/script.js")));
+
+            // A subresource request without embedded credentials should be
+            // allowed.
+            EXPECT_FALSE(
+                fetch_context->ShouldBlockFetchAsCredentialedSubresource(
+                    script_request, KURL("http://b.test/script.js")));
+
+            // An XMLHTTPRequest with embedded credentials should be allowed.
+            ResourceRequest xhr_request;
+            xhr_request.SetRequestContext(
+                mojom::blink::RequestContextType::XML_HTTP_REQUEST);
+            EXPECT_FALSE(
+                fetch_context->ShouldBlockFetchAsCredentialedSubresource(
+                    xhr_request, KURL("http://user:pass@b.test/script.js")));
+
+            std::move(quit_closure).Run();
+          },
+          CrossThreadUnretained(GetWorkerThread()),
+          CrossThreadOnceClosure(run_loop.QuitClosure())));
+  run_loop.Run();
+}
+
 class DedicatedWorkerDocumentPolicyTest
     : public DedicatedWorkerTest,
       public testing::WithParamInterface<bool> {
Loading diff…

Original Bug Report

reported by vm...@google.com

Bypass of BlockCredentialedSubresources intervention in WorkerFetchContext

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic error in WorkerFetchContext allows cross-origin subresource requests with embedded credentials if they match the worker’s credentials. This omits a critical same-origin check present in FrameFetchContext, potentially enabling authenticated CSRF attacks from workers. The issue affects dedicated, shared, and service workers.

Affected files:

  • third_party/blink/renderer/core/loader/worker_fetch_context.cc

Estimated timestamp from git blame: 2017-06-13

Summary

A potential vulnerability exists in the implementation of WorkerFetchContext::ShouldBlockFetchAsCredentialedSubresource that may allow an attacker to bypass the BlockCredentialedSubresources intervention. This intervention is intended to prevent CSRF attacks by blocking subresource requests that contain embedded credentials (e.g., https://user:pass@example.com/).

While FrameFetchContext requires that a credentialed subresource request both have matching credentials AND be same-origin with the document, the worker implementation currently only checks if the credentials match the worker’s URL, omitting the same-origin requirement. This could allow cross-origin importScripts() calls to carry embedded credentials to a victim server.

Root Cause Analysis

In third_party/blink/renderer/core/loader/worker_fetch_context.cc, the method ShouldBlockFetchAsCredentialedSubresource handles the check as follows:

// third_party/blink/renderer/core/loader/worker_fetch_context.cc:158
bool WorkerFetchContext::ShouldBlockFetchAsCredentialedSubresource(
    const ResourceRequest& resource_request,
    const KURL& url) const {
  if ((!url.User().empty() || !url.Pass().empty()) &&
      resource_request.GetRequestContext() !=
          mojom::blink::RequestContextType::XML_HTTP_REQUEST) {
    if (Url().User() != url.User() || Url().Pass() != url.Pass()) {
      // ... blocking logic ...
      return true;
    }
  }
  return false; // Credentials match -> ALLOW, missing same-origin check
}

In contrast, the document-based implementation in third_party/blink/renderer/core/loader/frame_fetch_context.cc (lines 1241-1247) correctly enforces a same-origin requirement:

if (Url().User() == url.User() && Url().Pass() == url.Pass() &&
    SecurityOrigin::Create(url)->IsSameOriginWith(
        GetResourceFetcherProperties()
            .GetFetchClientSettingsObject()
            .GetSecurityOrigin())) {
  return false;
}

Because the worker context lacks the IsSameOriginWith check, a worker initialized with a credentialed URL (e.g., via a parent page navigation) can successfully issue authenticated requests to a cross-origin target that shares the same credentials.

Potential Impact

An attacker could potentially exploit this to perform authenticated CSRF against services using Basic or Digest authentication, such as routers, IoT devices, or internal enterprise applications. If an attacker can convince a user to navigate to https://admin:admin@attacker.com, they can spawn a worker that executes importScripts('https://admin:admin@router.local/config/reboot'). The browser would then send an authenticated request to the local router, bypassing the intended security restrictions on credentialed subresources.

Potential Reproduction Steps

  1. Host a page at http://attacker.example/index.html that creates a worker: new Worker('worker.js');.
  2. Host http://attacker.example/worker.js with: importScripts('http://admin:admin@192.168.0.1/admin/reboot');.
  3. Induce a user to navigate to http://admin:admin@attacker.example/index.html (the user may need to accept a browser navigation warning).
  4. The worker is initialized with the URL http://admin:admin@attacker.example/worker.js.
  5. The worker calls importScripts(). WorkerFetchContext observes matching credentials (admin:admin) and allows the cross-origin request due to the missing origin check.
  6. The browser retries the request to the target IP with a Basic Authentication header if challenged.

Suggested Fix

Update WorkerFetchContext::ShouldBlockFetchAsCredentialedSubresource to include a same-origin check similar to FrameFetchContext. The request should only be allowed if the credentials match AND the subresource URL is same-origin with the worker’s security origin.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker