CVE-2026-17736
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fservices/network/cors/cors_url_loader_factory_unittest.cc |
modified |
Files Changed
services/network/cors/cors_url_loader_factory.ccservices/network/cors/cors_url_loader_factory_unittest.ccservices/network/public/cpp/features.ccservices/network/public/cpp/features.h
Patch
From cb9962e1927368f3095d18ede0fc8bf70d982258 Mon Sep 17 00:00:00 2001 From: Nate Fischer <[email protected]> Date: Mon, 29 Jun 2026 14:06:52 -0700 Subject: [PATCH] Enforce frame destinations require kNavigate mode A compromised renderer could spoof the request destination to be a frame type (e.g. kDocument) on a subresource request (e.g. mode = kNoCors). This could trick downstream consumers (like Android WebView's shouldInterceptRequest) into misclassifying it as a main-frame navigation, potentially bypassing security checks. This CL updates CorsURLLoaderFactory::IsValidRequest to reject requests with frame-type destinations (kDocument, kFrame, kIframe, kFencedframe) if the mode is not kNavigate. This omits kObject and kEmbed because these can be used for subresources (which are not navigations). TAG=agy CONV=032c4738-cca6-4b91-b39f-2216dc1e6f75 Fixed: 496715442 Test: run_services_unittests --gtest_filter=*NavigateMode Change-Id: I5b0b2acf4d0b9cfc10a5ef934ae1f27269b70529 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7952612 Reviewed-by: Takashi Toyoshima <[email protected]> Auto-Submit: Nate Fischer <[email protected]> Commit-Queue: Nate Fischer <[email protected]> Cr-Commit-Position: refs/heads/main@{#1654353} --- diff --git a/services/network/cors/cors_url_loader_factory.cc b/services/network/cors/cors_url_loader_factory.cc index 77e200a..82cc691 100644 --- a/services/network/cors/cors_url_loader_factory.cc +++ b/services/network/cors/cors_url_loader_factory.cc @@ -679,6 +679,31 @@ return false; } + // A request whose destination is a frame type must be a navigation. A + // renderer-initiated subresource fetch must never claim to be a document / + // frame load, otherwise downstream consumers (e.g. Android WebView's + // shouldInterceptRequest) may misclassify it as a main-frame navigation. + // See 2.2.5 Requests: https://fetch.spec.whatwg.org/#requests + // See Navigation Request: https://fetch.spec.whatwg.org/#navigation-request + // See fenced frames: https://github.com/WICG/fenced-frame/issues/239 + // + // This intentionally excludes destination types (see + // https://chromium-review.git.corp.google.com/c/chromium/src/+/7952612?tab=checks + // for details): + // * kEmbed: used by PDF pages to embed subresources. + // * kObject: used by wpt tests. + if (base::FeatureList::IsEnabled( + features::kRestrictFrameDestinationsToNavigate) && + (request.destination == mojom::RequestDestination::kDocument || + request.destination == mojom::RequestDestination::kFrame || + request.destination == mojom::RequestDestination::kIframe || + request.destination == mojom::RequestDestination::kFencedframe) && + request.mode != mojom::RequestMode::kNavigate) { + mojo::ReportBadMessage( + "CorsURLLoaderFactory: frame destination requires kNavigate mode"); + return false; + } + // Validate that a navigation redirect chain is not sent for a non-navigation // request. if (!request.navigation_redirect_chain.empty() && diff --git a/services/network/cors/cors_url_loader_factory_unittest.cc b/services/network/cors/cors_url_loader_factory_unittest.cc index d7e0e82..6c9a137e8 100644 --- a/services/network/cors/cors_url_loader_factory_unittest.cc +++ b/services/network/cors/cors_url_loader_factory_unittest.cc @@ -265,6 +265,24 @@ bad_message_observer.WaitForBadMessage()); } +TEST_F(CorsURLLoaderFactoryTest, DocumentDestinationRequiresNavigateMode) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature( + features::kRestrictFrameDestinationsToNavigate); + + ResourceRequest request; + request.mode = mojom::RequestMode::kNoCors; + request.credentials_mode = mojom::CredentialsMode::kOmit; + request.method = net::HttpRequestHeaders::kGetMethod; + request.url = test_server()->GetURL("/echoall"); + request.destination = mojom::RequestDestination::kDocument; + request.request_initiator = url::Origin::Create(request.url); + mojo::test::BadMessageObserver bad_message_observer; + CreateLoaderAndStart(request); + EXPECT_EQ("CorsURLLoaderFactory: frame destination requires kNavigate mode", + bad_message_observer.WaitForBadMessage()); +} + TEST_F(CorsURLLoaderFactoryTest, NavigationFromRendererWithBadRequestURLOrigin) { ResourceRequest request; diff --git a/services/network/public/cpp/features.cc b/services/network/public/cpp/features.cc index 7aa0817..4fc92a1 100644 --- a/services/network/public/cpp/features.cc +++ b/services/network/public/cpp/features.cc @@ -199,6 +199,10 @@ // Ignore CorsPreflightPolicy and always perform CORS checks. BASE_FEATURE(kIgnoreCorsPreflightPolicy, base::FEATURE_ENABLED_BY_DEFAULT); +// Enforces that frame-type destinations require kNavigate mode. +BASE_FEATURE(kRestrictFrameDestinationsToNavigate, + base::FEATURE_ENABLED_BY_DEFAULT); + // Enables support for the `Variants` response header and reduce // accept-language. https://github.com/Tanych/accept-language BASE_FEATURE(kReduceAcceptLanguage, base::FEATURE_DISABLED_BY_DEFAULT); diff --git a/services/network/public/cpp/features.h b/services/network/public/cpp/features.h index fa5ee5e..638779f 100644 --- a/services/network/public/cpp/features.h +++ b/services/network/public/cpp/features.h @@ -84,6 +84,9 @@ BASE_DECLARE_FEATURE(kIgnoreCorsPreflightPolicy); COMPONENT_EXPORT(NETWORK_CPP_FLAGS_AND_SWITCHES) +BASE_DECLARE_FEATURE(kRestrictFrameDestinationsToNavigate); + +COMPONENT_EXPORT(NETWORK_CPP_FLAGS_AND_SWITCHES) BASE_DECLARE_FEATURE(kReduceAcceptLanguage); COMPONENT_EXPORT(NETWORK_CPP_FLAGS_AND_SWITCHES) BASE_DECLARE_FEATURE(kReduceAcceptLanguageHTTP);
Regression Test / PoC
diff --git a/services/network/cors/cors_url_loader_factory_unittest.cc b/services/network/cors/cors_url_loader_factory_unittest.cc
index d7e0e82..6c9a137e8 100644
--- a/services/network/cors/cors_url_loader_factory_unittest.cc
+++ b/services/network/cors/cors_url_loader_factory_unittest.cc
@@ -265,6 +265,24 @@
bad_message_observer.WaitForBadMessage());
}
+TEST_F(CorsURLLoaderFactoryTest, DocumentDestinationRequiresNavigateMode) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(
+ features::kRestrictFrameDestinationsToNavigate);
+
+ ResourceRequest request;
+ request.mode = mojom::RequestMode::kNoCors;
+ request.credentials_mode = mojom::CredentialsMode::kOmit;
+ request.method = net::HttpRequestHeaders::kGetMethod;
+ request.url = test_server()->GetURL("/echoall");
+ request.destination = mojom::RequestDestination::kDocument;
+ request.request_initiator = url::Origin::Create(request.url);
+ mojo::test::BadMessageObserver bad_message_observer;
+ CreateLoaderAndStart(request);
+ EXPECT_EQ("CorsURLLoaderFactory: frame destination requires kNavigate mode",
+ bad_message_observer.WaitForBadMessage());
+}
+
TEST_F(CorsURLLoaderFactoryTest,
NavigationFromRendererWithBadRequestURLOrigin) {
ResourceRequest request;
Original Bug Report
Potential spoofing of isForMainFrame in WebView via forged request.destination
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A compromised renderer can forge a subresource request with destination = kDocument, bypassing CorsURLLoaderFactory validation. In Android WebView, this causes the shouldInterceptRequest callback to incorrectly identify the request as isForMainFrame() == true, allowing the renderer to bypass navigation checks and steal sensitive data.
Affected files:
android_webview/browser/network_service/aw_web_resource_request.ccservices/network/cors/cors_url_loader_factory.cc
Estimated timestamp from git blame: 2025-11-04
Summary
A potential vulnerability exists where a compromised renderer can craft a network::ResourceRequest that misleads the Android WebView shouldInterceptRequest callback into believing a subresource fetch is an outermost main-frame navigation (isForMainFrame() == true). This allows an attacker to bypass critical navigation-specific security checks (like shouldOverrideUrlLoading) and trick embedder applications into serving sensitive main-frame-only data directly to the renderer process.
Technical Details
1. Validation Bypass in Network Service:
The CorsURLLoaderFactory::IsValidRequest function in services/network/cors/cors_url_loader_factory.cc validates requests from unprivileged renderers. While it explicitly checks if request.original_destination == kDocument requires request.mode == kNavigate (lines 682-689), it fails to perform a reciprocal check for request.destination.
At line 776, kDocument is explicitly listed as an allowed destination from unprivileged processes. A compromised renderer can send a request with destination = kDocument, mode = kNoCors, and original_destination = kEmpty. Because original_destination is empty, the check enforcing navigation mode is bypassed, and the request is allowed through as a valid subresource fetch.
2. State Confusion in Android WebView:
When this request reaches Android WebView’s AwProxyingURLLoaderFactory, it is translated into an AwWebResourceRequest to be sent to the Java embedder (android_webview/browser/network_service/aw_web_resource_request.cc:25-26).
The is_outermost_main_frame boolean is calculated directly from the spoofed destination:
is_outermost_main_frame(request.destination == network::mojom::RequestDestination::kDocument)
Consequently, the Java shouldInterceptRequest callback receives the request and reports isForMainFrame() == true.
3. Bypassing Navigation Checks and ORB:
Because the request was initiated as a subresource fetch (mode = kNoCors), it completely bypasses the browser’s navigation stack. This means critical callbacks like shouldOverrideUrlLoading—which embedders rely on to block dangerous top-level navigations—are never triggered.
If the embedder trusts the isForMainFrame flag and provides a custom WebResourceResponse, the C++ layer serves it using AndroidStreamReaderURLLoader. Unlike the standard network::URLLoader, this custom loader does not implement Opaque Response Blocking (ORB) sniffing. The compromised renderer can thus read the sensitive response body without being blocked.
Potential Attacker Steps
(Note: These are potential steps as the Flapjack LLM agent cannot currently run or verify live exploit code.)
- Compromise Renderer: The attacker exploits a memory safety bug to gain arbitrary code execution in the WebView renderer process.
- Craft Malicious Request: Using the Mojo
URLLoaderFactoryinterface, the attacker constructs anetwork::ResourceRequesttargeting a sensitive URL (e.g., an internal app endpoint). - Spoof Properties: The attacker sets
destination = kDocument,mode = kNoCors, andoriginal_destination = kEmpty. - Send Request: The request is sent to the Network Service, cleanly bypassing
CorsURLLoaderFactory::IsValidRequest. - Bypass Navigation Stack: The request skips the standard navigation flow (and thus
shouldOverrideUrlLoading) but triggersshouldInterceptRequestin the Java embedder. - Deceive Embedder: The embedder app calls
request.isForMainFrame(), which returnstrue. Believing it is a safe top-level load, the app returns a response containing sensitive data (e.g., auth tokens). - Exfiltrate Data: The
AndroidStreamReaderURLLoaderstreams the response back to the renderer without ORB checks, allowing the attacker to read the sensitive data.
Suggested Fix
In services/network/cors/cors_url_loader_factory.cc, update IsValidRequest to enforce that whenever request.destination == mojom::RequestDestination::kDocument (or kFrame, kIframe, etc.), the request.mode must be mojom::RequestMode::kNavigate.
if (request.destination == mojom::RequestDestination::kDocument &&
request.mode != mojom::RequestMode::kNavigate) {
mojo::ReportBadMessage("CorsURLLoaderFactory: destination kDocument requires mode kNavigate");
return false;
}
This prevents compromised renderers from spoofing frame types on subresource requests.
Evaluated with Chrome root at commit: False
Results from Fortify 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 Fortify’s accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.