Medium watch CVSS 5.3 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactConfused deputy in BackgroundFetch
DescriptionConfused deputy in BackgroundFetch
ComponentBackgroundFetch
Sub-componentBackgroundFetch
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.
Tracker495444970
Fix commit8046f4cc56ed (chromium/src) +77/-1
CWECWE-441
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-23 (per vendor advisory)
Fix landed2026-08-07 (public git)
Disclosed2026-09-08
Patch gap32 days — fix public in source before the advisory

Background

Background Fetch
a web platform API letting a service worker register long-running downloads; the renderer’s BackgroundFetchManager builds the request list and the browser-process BackgroundFetchServiceImpl services the Mojo Fetch() call.
`BackgroundFetchServiceImpl::ValidateRequests()`
the browser-side validator that checks a renderer-supplied vector of blink::mojom::FetchAPIRequestPtr before the fetch proceeds.
Confused deputy
the browser process holds privileges the renderer lacks; if it forwards a renderer-chosen request to the download subsystem, that request is treated as a trusted browser-initiated request.
`GURL::SchemeIsHTTPOrHTTPS()`
the check that a URL uses the http/https scheme, distinguishing web-fetchable URLs from privileged schemes such as file:.

Root Cause Analysis

BackgroundFetchServiceImpl::ValidateRequests() validated other properties of the incoming request list but never checked that each request’s URL was non-null, valid, and HTTP(S), even though the renderer’s BackgroundFetchManager already rejects non-HTTP(S) request URLs. Because a compromised renderer can bypass its own renderer-side check and send arbitrary Mojo FetchAPIRequests, the browser could accept a request whose URL used a privileged scheme (e.g. file:///test.txt) and forward it to the download subsystem, which would treat it as a trusted browser request.

The fix mirrors the renderer’s constraint in the browser: ValidateRequests() now iterates the requests and, for any that is null, has an invalid url, or whose url is not SchemeIsHTTPOrHTTPS(), calls mojo::ReportBadMessage and returns false, so the browser rejects the message before dispatch. This re-establishes the invariant that the privileged download path only ever fetches web-scheme URLs supplied through a trusted channel.

Key insight
The single mistake was trusting the renderer to enforce the HTTP(S)-only scheme restriction and omitting it from the browser-side ValidateRequests(), letting a compromised renderer smuggle a privileged-scheme URL into the trusted download path; the fix duplicates the scheme (and validity/null) check in the browser process.

Attack Path

  1. Compromised renderer An attacker with code execution in a renderer bypasses the renderer-side BackgroundFetchManager scheme rejection and constructs the Mojo Fetch() message directly.
  2. Send a privileged-scheme request The message carries a FetchAPIRequest whose url uses a non-HTTP(S) scheme such as file:///test.txt (the exact case exercised by the regression test).
  3. Browser fails to validate Pre-patch, ValidateRequests() returns true for the request because it never inspected the URL scheme, so the browser proceeds.
  4. Deputy fetch The browser forwards the request to the download subsystem, which treats it as a trusted browser-initiated request for the privileged URL (inferred: reading a local file: resource and returning its contents to the initiating context, the information-disclosure outcome).

Impact Assessment

A compromised renderer gains a confused-deputy primitive in which the privileged browser process fetches a URL of the renderer’s choosing, including privileged schemes like file:, via the download subsystem; the precondition is the ability to send crafted Background Fetch Mojo messages from a renderer. The advisory classifies this as Confused deputy in BackgroundFetch and does not report in-the-wild exploitation. The concrete downstream effect (e.g. local-file read / information leak) is the plausible consequence of forwarding a file: request but is not itself demonstrated by the diff (inferred).

Changed Functions

FunctionChangeNotes
for
content/browser/background_fetch/background_fetch_service_impl.cc
modified
if
content/browser/background_fetch/background_fetch_service_impl.cc
modified
TEST_F
content/browser/background_fetch/background_fetch_service_unittest.cc
modified

Files Changed

  • content/browser/background_fetch/background_fetch_service_unittest.cc
  • content/browser/background_fetch/background_fetch_service_impl.cc

Audit Directions

  • Renderer-side check not mirrored in browser
    Audit browser Mojo endpoints whose validation was implemented only in the renderer (BackgroundFetchManager here); each is a candidate for bypass by a compromised renderer and should re-validate scheme/origin/nullness server-side.
  • URL scheme reaching privileged subsystems
    Trace renderer-supplied GURLs that flow into download, file, or network subsystems and confirm an explicit SchemeIsHTTPOrHTTPS() (or narrower) check gates privileged handling.
  • Null and validity checks on Mojo collections
    Review handlers that accept vectors of mojom pointers for missing null-element and is_valid() checks before use, since Mojo does not guarantee non-null or well-formed elements.
From 8046f4cc56ed8aebe6c9aa4838f5ae6b1ba07764 Mon Sep 17 00:00:00 2001
From: Nidhi Jaju <[email protected]>
Date: Fri, 07 Aug 2026 04:08:03 -0700
Subject: [PATCH] [BackgroundFetch] Validate request URL scheme in the browser

BackgroundFetchManager in the renderer rejects request URLs that do not
use a HTTP(S) scheme. Mirror that check in
BackgroundFetchServiceImpl::ValidateRequests() so the browser process
does not forward such requests to the download subsystem, which would
otherwise treat them as trusted browser requests.

Bug: 495444970
Change-Id: If25e925d5d7e19a9d53453971933dc0d10d9a4a1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8200209
Commit-Queue: Peter Beverloo <[email protected]>
Auto-Submit: Nidhi Jaju <[email protected]>
Reviewed-by: Peter Beverloo <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1675631}
---
diff --git a/content/browser/background_fetch/background_fetch_service_impl.cc b/content/browser/background_fetch/background_fetch_service_impl.cc
index f2e3d9fd..97f0ba9 100644
--- a/content/browser/background_fetch/background_fetch_service_impl.cc
+++ b/content/browser/background_fetch/background_fetch_service_impl.cc
@@ -222,6 +222,24 @@
     return false;
   }
 
+  // Ensure all requests are valid and use the HTTP or HTTPS scheme.
+  for (const auto& request : requests) {
+    if (!request) {
+      mojo::ReportBadMessage("Null request");
+      return false;
+    }
+
+    if (!request->url.is_valid()) {
+      mojo::ReportBadMessage("Invalid request URL");
+      return false;
+    }
+
+    if (!request->url.SchemeIsHTTPOrHTTPS()) {
+      mojo::ReportBadMessage("Invalid request URL scheme");
+      return false;
+    }
+  }
+
   return true;
 }
 
diff --git a/content/browser/background_fetch/background_fetch_service_unittest.cc b/content/browser/background_fetch/background_fetch_service_unittest.cc
index 287e7ec5..9fb5ec3a 100644
--- a/content/browser/background_fetch/background_fetch_service_unittest.cc
+++ b/content/browser/background_fetch/background_fetch_service_unittest.cc
@@ -493,11 +493,69 @@
     blink::mojom::BackgroundFetchRegistrationPtr registration;
 
     Fetch(/* service_worker_registration_id= */ 42, kExampleDeveloperId,
-          std::move(requests), std::move(options), SkBitmap(), &error,
+          std::move(requests), options.Clone(), SkBitmap(), &error,
           &registration);
     ASSERT_EQ(error, blink::mojom::BackgroundFetchError::INVALID_ARGUMENT);
     EXPECT_EQ("Invalid requests", bad_message_observer.WaitForBadMessage());
   }
+
+  // Request URLs must use the HTTP or HTTPS scheme.
+  {
+    mojo::FakeMessageDispatchContext fake_dispatch_context;
+    mojo::test::BadMessageObserver bad_message_observer;
+    std::vector<blink::mojom::FetchAPIRequestPtr> requests;
+    auto request = CreateRequestWithProvidedResponse(
+        "GET", GURL("file:///test.txt"),
+        TestResponseBuilder(200).MakeIndefinitelyPending().Build());
+    requests.push_back(std::move(request));
+
+    blink::mojom::BackgroundFetchError error;
+    blink::mojom::BackgroundFetchRegistrationPtr registration;
+
+    Fetch(/* service_worker_registration_id= */ 42, kExampleDeveloperId,
+          std::move(requests), options.Clone(), SkBitmap(), &error,
+          &registration);
+    EXPECT_EQ(error, blink::mojom::BackgroundFetchError::INVALID_ARGUMENT);
+    EXPECT_EQ("Invalid request URL scheme",
+              bad_message_observer.WaitForBadMessage());
+  }
+
+  // Request URLs must be valid URLs.
+  {
+    mojo::FakeMessageDispatchContext fake_dispatch_context;
+    mojo::test::BadMessageObserver bad_message_observer;
+    std::vector<blink::mojom::FetchAPIRequestPtr> requests;
+    auto request = CreateRequestWithProvidedResponse(
+        "GET", GURL("invalid-url"),
+        TestResponseBuilder(200).MakeIndefinitelyPending().Build());
+    requests.push_back(std::move(request));
+
+    blink::mojom::BackgroundFetchError error;
+    blink::mojom::BackgroundFetchRegistrationPtr registration;
+
+    Fetch(/* service_worker_registration_id= */ 42, kExampleDeveloperId,
+          std::move(requests), options.Clone(), SkBitmap(), &error,
+          &registration);
+    EXPECT_EQ(error, blink::mojom::BackgroundFetchError::INVALID_ARGUMENT);
+    EXPECT_EQ("Invalid request URL", bad_message_observer.WaitForBadMessage());
+  }
+
+  // Request objects in the vector must be non-null.
+  {
+    mojo::FakeMessageDispatchContext fake_dispatch_context;
+    mojo::test::BadMessageObserver bad_message_observer;
+    std::vector<blink::mojom::FetchAPIRequestPtr> requests;
+    requests.push_back(nullptr);
+
+    blink::mojom::BackgroundFetchError error;
+    blink::mojom::BackgroundFetchRegistrationPtr registration;
+
+    Fetch(/* service_worker_registration_id= */ 42, kExampleDeveloperId,
+          std::move(requests), std::move(options), SkBitmap(), &error,
+          &registration);
+    EXPECT_EQ(error, blink::mojom::BackgroundFetchError::INVALID_ARGUMENT);
+    EXPECT_EQ("Null request", bad_message_observer.WaitForBadMessage());
+  }
 }
 
 TEST_F(BackgroundFetchServiceTest, FetchRegistrationProperties) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/background_fetch/background_fetch_service_unittest.cc b/content/browser/background_fetch/background_fetch_service_unittest.cc
index 287e7ec5..9fb5ec3a 100644
--- a/content/browser/background_fetch/background_fetch_service_unittest.cc
+++ b/content/browser/background_fetch/background_fetch_service_unittest.cc
@@ -493,11 +493,69 @@
     blink::mojom::BackgroundFetchRegistrationPtr registration;
 
     Fetch(/* service_worker_registration_id= */ 42, kExampleDeveloperId,
-          std::move(requests), std::move(options), SkBitmap(), &error,
+          std::move(requests), options.Clone(), SkBitmap(), &error,
           &registration);
     ASSERT_EQ(error, blink::mojom::BackgroundFetchError::INVALID_ARGUMENT);
     EXPECT_EQ("Invalid requests", bad_message_observer.WaitForBadMessage());
   }
+
+  // Request URLs must use the HTTP or HTTPS scheme.
+  {
+    mojo::FakeMessageDispatchContext fake_dispatch_context;
+    mojo::test::BadMessageObserver bad_message_observer;
+    std::vector<blink::mojom::FetchAPIRequestPtr> requests;
+    auto request = CreateRequestWithProvidedResponse(
+        "GET", GURL("file:///test.txt"),
+        TestResponseBuilder(200).MakeIndefinitelyPending().Build());
+    requests.push_back(std::move(request));
+
+    blink::mojom::BackgroundFetchError error;
+    blink::mojom::BackgroundFetchRegistrationPtr registration;
+
+    Fetch(/* service_worker_registration_id= */ 42, kExampleDeveloperId,
+          std::move(requests), options.Clone(), SkBitmap(), &error,
+          &registration);
+    EXPECT_EQ(error, blink::mojom::BackgroundFetchError::INVALID_ARGUMENT);
+    EXPECT_EQ("Invalid request URL scheme",
+              bad_message_observer.WaitForBadMessage());
+  }
+
+  // Request URLs must be valid URLs.
+  {
+    mojo::FakeMessageDispatchContext fake_dispatch_context;
+    mojo::test::BadMessageObserver bad_message_observer;
+    std::vector<blink::mojom::FetchAPIRequestPtr> requests;
+    auto request = CreateRequestWithProvidedResponse(
+        "GET", GURL("invalid-url"),
+        TestResponseBuilder(200).MakeIndefinitelyPending().Build());
+    requests.push_back(std::move(request));
+
+    blink::mojom::BackgroundFetchError error;
+    blink::mojom::BackgroundFetchRegistrationPtr registration;
+
+    Fetch(/* service_worker_registration_id= */ 42, kExampleDeveloperId,
+          std::move(requests), options.Clone(), SkBitmap(), &error,
+          &registration);
+    EXPECT_EQ(error, blink::mojom::BackgroundFetchError::INVALID_ARGUMENT);
+    EXPECT_EQ("Invalid request URL", bad_message_observer.WaitForBadMessage());
+  }
+
+  // Request objects in the vector must be non-null.
+  {
+    mojo::FakeMessageDispatchContext fake_dispatch_context;
+    mojo::test::BadMessageObserver bad_message_observer;
+    std::vector<blink::mojom::FetchAPIRequestPtr> requests;
+    requests.push_back(nullptr);
+
+    blink::mojom::BackgroundFetchError error;
+    blink::mojom::BackgroundFetchRegistrationPtr registration;
+
+    Fetch(/* service_worker_registration_id= */ 42, kExampleDeveloperId,
+          std::move(requests), std::move(options), SkBitmap(), &error,
+          &registration);
+    EXPECT_EQ(error, blink::mojom::BackgroundFetchError::INVALID_ARGUMENT);
+    EXPECT_EQ("Null request", bad_message_observer.WaitForBadMessage());
+  }
 }
 
 TEST_F(BackgroundFetchServiceTest, FetchRegistrationProperties) {
Loading diff…

Original Bug Report

reported by [email protected]

Local file existence oracle and NTLM leak via Background Fetch

Flapjack (go/flapjack), an LLM-powered static analysis tool, has identified the following potential security issue.

Overview: A compromised renderer can initiate Background Fetch requests for file:// URLs due to missing scheme validation in the browser process. These requests bypass download security checks because the requesting process ID is lost during component handoff, allowing the browser to act as a confused deputy. This can potentially be abused to leak NTLM credentials via UNC paths on Windows or act as a cross-platform local file existence oracle.

Affected files:

  • content/browser/background_fetch/background_fetch_service_impl.cc
  • content/browser/background_fetch/background_fetch_context.cc
  • content/browser/background_fetch/background_fetch_data_manager.cc
  • components/download/content/internal/download_driver_impl.cc
  • content/browser/download/download_manager_impl.cc

Estimated timestamp from git blame: 2021-08-03

Summary

A compromised renderer process can bypass Blink-side restrictions and directly send a Mojo message to the browser process to initiate a Background Fetch for file:// URLs. Due to missing scheme validation in BackgroundFetchServiceImpl::ValidateRequests and a loss of the renderer’s process ID during the handoff to the Download subsystem, the browser process skips standard security checks (DownloadRequestUtils::IsURLSafe). This allows the browser to act as a confused deputy and fetch local files or UNC paths.

While the contents of the local files are not directly exposed to the Service Worker due to CORS filtering (BackgroundFetchCrossOriginFilter), the attacker can distinguish between a file existing (CORS error) and not existing (network error). Furthermore, on Windows, fetching a UNC path will cause the browser process to automatically attempt SMB authentication, leaking the user’s NTLM credentials.

Technical Details

  1. Missing Scheme Validation: When a renderer initiates a fetch via Mojo, BackgroundFetchServiceImpl::Fetch calls ValidateRequests (content/browser/background_fetch/background_fetch_service_impl.cc). This function currently only checks if the requests vector is empty. It fails to validate that the URLs use a safe scheme (e.g., HTTP/HTTPS).
  2. Loss of Process Attribution: The request is passed through BackgroundFetchDelegateBase to the BackgroundDownloadService. When the download is ready, DownloadDriverImpl::Start (components/download/content/internal/download_driver_impl.cc) instantiates DownloadUrlParameters using a constructor that defaults render_process_host_id and render_frame_host_routing_id to -1.
  3. Security Check Bypass: In DownloadManagerImpl::BeginDownloadInternal (content/browser/download/download_manager_impl.cc), the system attempts to verify if the renderer is allowed to access the URL:
    if (params->render_process_host_id() >= 0 &&
        !DownloadRequestUtils::IsURLSafe(params->render_process_host_id(), params->url())) { ... }
    
    Because the ID was defaulted to -1, this check is completely bypassed.
  4. Privileged File Fetching: The download proceeds to BeginResourceDownloadOnChecksComplete, which sees the file:// scheme and creates a FileURLLoaderFactory with the full permissions of the browser context.
  5. CORS Bypass at the Loader Level: The download subsystem (InProgressDownloadManager) hardcodes the network request mode to network::mojom::RequestMode::kNavigate. Because of this, CalculateResponseType in services/network/public/cpp/cors/cors.cc returns kBasic. This prevents FileURLLoaderFactory::CreateLoaderAndStartInternal from rejecting the cross-origin file:// request upfront.
  6. The Oracle/Leak:
    • NTLM Leak: If a Windows UNC path is provided (file:////attacker.com/share/test), net::FileURLToFilePath parses it as \\attacker.com\share\test, and the OS attempts NTLM authentication with the remote server.
    • Existence Oracle: The FileURLLoader attempts to read the file. If it succeeds, the fetch completes, but BackgroundFetchCrossOriginFilter::CanPopulateBody() blocks the response body from the Service Worker cache due to missing CORS headers, resulting in a FETCH_ERROR state. If the file does not exist, the loader fails immediately with ERR_FILE_NOT_FOUND, resulting in a BAD_STATUS state. The attacker can observe these differing states via the Service Worker Background Fetch events.

Potential Attacker Steps (Suggested)

Note: These are potential steps to trigger the vulnerability, as Flapjack LLM agents do not yet have the ability to run code to provide a working PoC.

  1. Gain arbitrary code execution in a sandboxed renderer process (e.g., via a V8 or Blink memory corruption bug).
  2. Establish a direct Mojo connection to blink::mojom::BackgroundFetchService.
  3. Craft a FetchAPIRequestPtr containing a target file:// URL (e.g., a local file to probe for existence, or a remote UNC path to steal NTLM hashes).
  4. Call BackgroundFetchService::Fetch with the crafted request.
  5. Register a Service Worker to listen for backgroundfetchsuccess and backgroundfetchfail events.
  6. Observe the error reason in the event to oracle file existence or capture the NTLM hash on a controlled remote SMB server.

Suggested Fix

  1. In BackgroundFetchServiceImpl::ValidateRequests, add strict validation to ensure all requested URLs use safe schemes (e.g., url.SchemeIsHTTPOrHTTPS()).
  2. Plumb the render_process_host_id and render_frame_host_routing_id through the BackgroundDownloadService stack so that DownloadDriverImpl::Start can populate DownloadUrlParameters accurately, ensuring DownloadRequestUtils::IsURLSafe is correctly evaluated for all downloads.

Evaluated with Chrome root at commit: 9760e6c70cd33a320713361f17c6dcca85648c0f


Results from Flapjack so far have been promising, but it can be wrong in its deductions. At this time, it does not produce proof of concepts or fuzzer tests. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve Flapjack’s 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