CVE-2026-87453
Overview
Background
- Background Fetch
- a web platform API letting a service worker register long-running downloads; the renderer’s
BackgroundFetchManagerbuilds the request list and the browser-processBackgroundFetchServiceImplservices the MojoFetch()call. - `BackgroundFetchServiceImpl::ValidateRequests()`
- the browser-side validator that checks a renderer-supplied vector of
blink::mojom::FetchAPIRequestPtrbefore 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/httpsscheme, distinguishing web-fetchable URLs from privileged schemes such asfile:.
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.
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
- Compromised renderer
An attacker with code execution in a renderer bypasses the renderer-side
BackgroundFetchManagerscheme rejection and constructs the MojoFetch()message directly. - Send a privileged-scheme request
The message carries a
FetchAPIRequestwhoseurluses a non-HTTP(S) scheme such asfile:///test.txt(the exact case exercised by the regression test). - Browser fails to validate
Pre-patch,
ValidateRequests()returnstruefor the request because it never inspected the URL scheme, so the browser proceeds. - 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
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
| Function | Change | Notes |
|---|---|---|
forcontent/browser/background_fetch/background_fetch_service_impl.cc |
modified | |
ifcontent/browser/background_fetch/background_fetch_service_impl.cc |
modified | |
TEST_Fcontent/browser/background_fetch/background_fetch_service_unittest.cc |
modified |
Files Changed
content/browser/background_fetch/background_fetch_service_unittest.cccontent/browser/background_fetch/background_fetch_service_impl.cc
Audit Directions
- Renderer-side check not mirrored in browserAudit browser Mojo endpoints whose validation was implemented only in the renderer (
BackgroundFetchManagerhere); each is a candidate for bypass by a compromised renderer and should re-validate scheme/origin/nullness server-side. - URL scheme reaching privileged subsystemsTrace renderer-supplied
GURLs that flow into download, file, or network subsystems and confirm an explicitSchemeIsHTTPOrHTTPS()(or narrower) check gates privileged handling. - Null and validity checks on Mojo collectionsReview handlers that accept vectors of
mojompointers for missing null-element andis_valid()checks before use, since Mojo does not guarantee non-null or well-formed elements.
Patch
From 8046f4cc56ed8aebe6c9aa4838f5ae6b1ba07764 Mon Sep 17 00:00:00 2001
From: Nidhi Jaju <nidhijaju@chromium.org>
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 <peter@chromium.org>
Auto-Submit: Nidhi Jaju <nidhijaju@chromium.org>
Reviewed-by: Peter Beverloo <peter@chromium.org>
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,
®istration);
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,
+ ®istration);
+ 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,
+ ®istration);
+ 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,
+ ®istration);
+ EXPECT_EQ(error, blink::mojom::BackgroundFetchError::INVALID_ARGUMENT);
+ EXPECT_EQ("Null request", bad_message_observer.WaitForBadMessage());
+ }
}
TEST_F(BackgroundFetchServiceTest, FetchRegistrationProperties) {
Regression Test / PoC
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,
®istration);
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,
+ ®istration);
+ 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,
+ ®istration);
+ 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,
+ ®istration);
+ EXPECT_EQ(error, blink::mojom::BackgroundFetchError::INVALID_ARGUMENT);
+ EXPECT_EQ("Null request", bad_message_observer.WaitForBadMessage());
+ }
}
TEST_F(BackgroundFetchServiceTest, FetchRegistrationProperties) {
Original Bug Report
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.cccontent/browser/background_fetch/background_fetch_context.cccontent/browser/background_fetch/background_fetch_data_manager.cccomponents/download/content/internal/download_driver_impl.cccontent/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
- Missing Scheme Validation: When a renderer initiates a fetch via Mojo,
BackgroundFetchServiceImpl::FetchcallsValidateRequests(content/browser/background_fetch/background_fetch_service_impl.cc). This function currently only checks if therequestsvector is empty. It fails to validate that the URLs use a safe scheme (e.g., HTTP/HTTPS). - Loss of Process Attribution: The request is passed through
BackgroundFetchDelegateBaseto theBackgroundDownloadService. When the download is ready,DownloadDriverImpl::Start(components/download/content/internal/download_driver_impl.cc) instantiatesDownloadUrlParametersusing a constructor that defaultsrender_process_host_idandrender_frame_host_routing_idto-1. - 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:Because the ID was defaulted toif (params->render_process_host_id() >= 0 && !DownloadRequestUtils::IsURLSafe(params->render_process_host_id(), params->url())) { ... }-1, this check is completely bypassed. - Privileged File Fetching: The download proceeds to
BeginResourceDownloadOnChecksComplete, which sees thefile://scheme and creates aFileURLLoaderFactorywith the full permissions of the browser context. - CORS Bypass at the Loader Level: The download subsystem (
InProgressDownloadManager) hardcodes the network request mode tonetwork::mojom::RequestMode::kNavigate. Because of this,CalculateResponseTypeinservices/network/public/cpp/cors/cors.ccreturnskBasic. This preventsFileURLLoaderFactory::CreateLoaderAndStartInternalfrom rejecting the cross-originfile://request upfront. - The Oracle/Leak:
- NTLM Leak: If a Windows UNC path is provided (
file:////attacker.com/share/test),net::FileURLToFilePathparses it as\\attacker.com\share\test, and the OS attempts NTLM authentication with the remote server. - Existence Oracle: The
FileURLLoaderattempts to read the file. If it succeeds, the fetch completes, butBackgroundFetchCrossOriginFilter::CanPopulateBody()blocks the response body from the Service Worker cache due to missing CORS headers, resulting in aFETCH_ERRORstate. If the file does not exist, the loader fails immediately withERR_FILE_NOT_FOUND, resulting in aBAD_STATUSstate. The attacker can observe these differing states via the Service Worker Background Fetch events.
- NTLM Leak: If a Windows UNC path is provided (
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.
- Gain arbitrary code execution in a sandboxed renderer process (e.g., via a V8 or Blink memory corruption bug).
- Establish a direct Mojo connection to
blink::mojom::BackgroundFetchService. - Craft a
FetchAPIRequestPtrcontaining a targetfile://URL (e.g., a local file to probe for existence, or a remote UNC path to steal NTLM hashes). - Call
BackgroundFetchService::Fetchwith the crafted request. - Register a Service Worker to listen for
backgroundfetchsuccessandbackgroundfetchfailevents. - Observe the error reason in the event to oracle file existence or capture the NTLM hash on a controlled remote SMB server.
Suggested Fix
- In
BackgroundFetchServiceImpl::ValidateRequests, add strict validation to ensure all requested URLs use safe schemes (e.g.,url.SchemeIsHTTPOrHTTPS()). - Plumb the
render_process_host_idandrender_frame_host_routing_idthrough theBackgroundDownloadServicestack so thatDownloadDriverImpl::Startcan populateDownloadUrlParametersaccurately, ensuringDownloadRequestUtils::IsURLSafeis 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.