Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Loader
DescriptionIncorrect authorization in Loader
ComponentLoader
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.
Tracker513346220
Fix commit03b026a35f12 (chromium/src) +114/-11
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-14 (per vendor advisory)
Fix landed2026-07-31 (public git)
Disclosed2026-09-08
Patch gap39 days — fix public in source before the advisory

Background

`WorkerFetchContext::ShouldBlockFetchAsCredentialedSubresource`
the Blink loader hook that decides whether a worker subresource request carrying embedded userinfo credentials (user:pass@host in the URL) should be blocked.
Embedded credentials
the url.User() / url.Pass() components of a URL; historically deprecated for subresource requests via WebFeature::kRequestedSubresourceWithEmbeddedCredentials.
Fetch client settings object
the security context of the fetch client, whose GetSecurityOrigin() (via GetResourceFetcherProperties().GetFetchClientSettingsObject()) is the authoritative origin that a target should be compared against for same-origin decisions.
`SecurityOrigin::IsSameOriginWith`
the origin-comparison primitive used by the fix to confirm the requested url’s origin matches the worker’s fetch-client origin.

Root Cause Analysis

Pre-patch, WorkerFetchContext::ShouldBlockFetchAsCredentialedSubresource blocked a credentialed non-XML_HTTP_REQUEST subresource only when Url().User() != url.User() || Url().Pass() != url.Pass() — i.e. when the embedded credentials differed from the worker script’s own URL credentials — and otherwise fell through to return false (allow). The removed code compared credentials but never compared origins, so a request whose credentials happened to match the worker’s own credentials was permitted even when the target url was a completely different (cross-origin) host. This violated the invariant that the matching-credentials exemption should apply only to same-origin subresources (relative URLs on a credentialed worker script), an invariant FrameFetchContext already enforced.

The fix restructures the function with explicit early returns and, before allowing the matching-credentials case, additionally requires SecurityOrigin::Create(url)->IsSameOriginWith(...GetFetchClientSettingsObject().GetSecurityOrigin()); any request that reaches the end without satisfying this falls through to CountDeprecation(...) and return true (block). This tightens the check: cross-origin credentialed subresources that were previously allowed are now blocked.

Key insight
The single mistake was exempting a credentialed subresource from blocking based solely on a credentials match against the worker’s own URL, without also verifying the target was same-origin. The fix adds the missing same-origin requirement (IsSameOriginWith against the fetch client settings object’s security origin) as a mandatory condition of the exemption.

Attack Path

  1. Load a credentialed worker An attacker page spawns a worker whose script URL carries embedded credentials (e.g. http://user:[email protected]/worker.js), establishing the worker’s Url().User()/Url().Pass().
  2. Request a cross-origin credentialed subresource From that worker, issue a non-XHR subresource fetch (e.g. RequestContextType::SCRIPT) to a different origin using the same embedded credentials, such as http://user:[email protected]/script.js.
  3. Pass the flawed exemption Pre-patch, ShouldBlockFetchAsCredentialedSubresource sees the credentials match Url().User()/Url().Pass(), skips the block branch, and returns false, so the credentialed cross-origin request is permitted instead of blocked/deprecated.
  4. Downstream escalation Any authorization or credential-handling consequence beyond the allowed cross-origin credentialed load (e.g. sending user-controlled credentials to an unintended origin) is (inferred); the diff only adds a same-origin guard and demonstrates the allow/block decision, not a further primitive.

Impact Assessment

An attacker gains the ability, from a credentialed worker in the renderer process, to have credentialed subresource requests sent to cross-origin targets that should have been blocked — an incorrect-authorization flaw in the Loader as the advisory describes (“Incorrect authorization in Loader”). The precondition is a worker whose own script URL carries embedded credentials, from which a same-credentials cross-origin non-XHR subresource is requested. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.3. The advisory states no exploited-in-the-wild status or severity, and the patch itself demonstrates only the allow-versus-block behavior, not memory corruption or code execution.

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
DedicatedWorkerDocumentPolicyTest
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

Audit Directions

  • Credentials-match-without-origin-check
    Search other FetchContext subclasses and subresource gates for exemptions keyed on matching embedded credentials or userinfo without a paired IsSameOriginWith / origin comparison against the fetch client settings object.
  • FrameFetchContext / WorkerFetchContext divergence
    Compare sibling BaseFetchContext implementations (frame vs. worker vs. worklet) for security decisions that one enforces but another omits, since this bug was a worker-side path lagging the frame-side check.
  • Fall-through allow paths
    Audit loader authorization functions where the default/terminal path return false (allow) is reached by falling out of a partial condition, and confirm every branch that should block is not silently bypassed when a narrower sub-condition matches.
From 03b026a35f127ea477f52e3ab645831107277624 Mon Sep 17 00:00:00 2001
From: Minoru Chikamune <[email protected]>
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 <[email protected]>
Reviewed-by: Shunya Shishido <[email protected]>
Auto-Submit: Minoru Chikamune <[email protected]>
Commit-Queue: Minoru Chikamune <[email protected]>
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:[email protected]/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:[email protected]/script.js")));
+
+            // A same-origin URL with non-matching embedded credentials must be
+            // blocked.
+            EXPECT_TRUE(
+                fetch_context->ShouldBlockFetchAsCredentialedSubresource(
+                    script_request,
+                    KURL("http://wrong:[email protected]/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:[email protected]/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:[email protected]/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…

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:[email protected]/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:[email protected]/script.js")));
+
+            // A same-origin URL with non-matching embedded credentials must be
+            // blocked.
+            EXPECT_TRUE(
+                fetch_context->ShouldBlockFetchAsCredentialedSubresource(
+                    script_request,
+                    KURL("http://wrong:[email protected]/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:[email protected]/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:[email protected]/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 [email protected]

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:[email protected]/).

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:[email protected], they can spawn a worker that executes importScripts('https://admin:[email protected]/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:[email protected]/admin/reboot');.
  3. Induce a user to navigate to http://admin:[email protected]/index.html (the user may need to accept a browser navigation warning).
  4. The worker is initialized with the URL http://admin:[email protected]/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