CVE-2026-79256
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifandroid_webview/browser/network_service/aw_proxying_url_loader_factory.cc |
modified | |
AwProxyingURLLoaderFactoryTestandroid_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc |
modified | |
test_url_loader_factory_android_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc |
modified | |
ifandroid_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc |
modified | |
TEST_Fandroid_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc |
modified |
Files Changed
android_webview/browser/network_service/aw_proxying_url_loader_factory.ccandroid_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc
Patch
From 751df049a7db402799a586c0d1fa92a64fe5be68 Mon Sep 17 00:00:00 2001 From: Joanne de Abreu <[email protected]> Date: Wed, 08 Jul 2026 03:07:34 -0700 Subject: [PATCH] Add browser side check for redirect to content/file urls There is a renderer side check to stop http/s urls redirecting to content/file urls. This cl adds a check on the browser side for android_webview to protect against a compromised renderer that bypasses such checks. Fixed: 499007248 Change-Id: Ifed48190982e55d4ed4d1122b80604509a169145 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7950860 Reviewed-by: Nate Fischer <[email protected]> Commit-Queue: Joanne de Abreu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1658621} --- diff --git a/android_webview/browser/network_service/aw_proxying_url_loader_factory.cc b/android_webview/browser/network_service/aw_proxying_url_loader_factory.cc index 7c29e9f..a640907 100644 --- a/android_webview/browser/network_service/aw_proxying_url_loader_factory.cc +++ b/android_webview/browser/network_service/aw_proxying_url_loader_factory.cc @@ -247,6 +247,7 @@ // error didn't occur. int error_status_ = net::OK; + GURL last_url_; network::ResourceRequest request_; const net::MutableNetworkTrafficAnnotationTag traffic_annotation_; @@ -368,6 +369,7 @@ options_(options), intercept_only_(intercept_only), security_options_(security_options), + last_url_(request.url), request_(std::move(request)), traffic_annotation_(traffic_annotation), proxied_loader_receiver_(this, std::move(loader_receiver)), @@ -793,6 +795,7 @@ network::mojom::URLResponseHeadPtr head) { // TODO(timvolodine): handle redirect override. request_was_redirected_ = true; + last_url_ = request_.url; target_client_->OnReceiveRedirect(redirect_info, std::move(head)); request_.url = redirect_info.new_url; request_.method = redirect_info.new_method; @@ -827,6 +830,14 @@ void InterceptedRequest::FollowRedirect( network::HttpRequestHeadersUpdateParams headers_update_params, const std::optional<GURL>& new_url) { + GURL target_url = new_url.value_or(request_.url); + if (request_was_redirected_ && + !content::IsSafeRedirectTarget(last_url_, target_url)) { + target_loader_.reset(); + SendErrorAndCompleteImmediately(net::ERR_UNSAFE_REDIRECT); + return; + } + if (target_loader_) { if (!origin_matched_headers_.empty()) { ApplyOriginMatchedHeaders(&headers_update_params.removed_headers, diff --git a/android_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc b/android_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc new file mode 100644 index 0000000..032caff --- /dev/null +++ b/android_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc @@ -0,0 +1,150 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "android_webview/browser/network_service/aw_proxying_url_loader_factory.h" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include "android_webview/browser/aw_origin_matched_header.h" +#include "android_webview/browser/network_service/aw_browser_context_io_thread_handle.h" +#include "base/functional/bind.h" +#include "base/run_loop.h" +#include "base/test/run_until.h" +#include "base/test/scoped_feature_list.h" +#include "content/public/test/browser_task_environment.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "net/base/net_errors.h" +#include "net/traffic_annotation/network_traffic_annotation_test_helper.h" +#include "services/network/public/cpp/http_request_headers_update_params.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/mojom/url_loader_factory.mojom.h" +#include "services/network/test/test_url_loader_client.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace android_webview { + +class AwProxyingURLLoaderFactoryTest : public testing::Test { + public: + AwProxyingURLLoaderFactoryTest() + : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP), + test_url_loader_factory_(true) {} + + protected: + int FollowRedirect(const GURL& original_url, const GURL& redirect_url) { + mojo::Remote<network::mojom::URLLoaderFactory> proxy_factory_remote; + mojo::Receiver<network::mojom::URLLoaderFactory> target_factory_receiver( + &test_url_loader_factory_); + + auto factory = std::make_unique<AwProxyingURLLoaderFactory>( + /*cookie_manager=*/std::nullopt, &cookie_access_policy_, + /*isolation_info=*/std::nullopt, + /*key=*/std::nullopt, content::FrameTreeNodeId(), + proxy_factory_remote.BindNewPipeAndPassReceiver(), + target_factory_receiver.BindNewPipeAndPassRemote(), + /*intercept_only=*/false, + /*security_options=*/std::nullopt, + /*origin_matched_headers=*/ + std::vector<scoped_refptr<AwOriginMatchedHeader>>(), + /*browser_context_handle=*/nullptr, + /*navigation_id=*/std::nullopt); + + network::ResourceRequest request; + request.url = original_url; + request.method = "GET"; + + mojo::Remote<network::mojom::URLLoader> loader_remote; + network::TestURLLoaderClient client; + + proxy_factory_remote->CreateLoaderAndStart( + loader_remote.BindNewPipeAndPassReceiver(), + /*request_id=*/1, + /*options=*/0, request, client.CreateRemote(), + net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS)); + + if (test_url_loader_factory_.NumPending() != 1) { + ADD_FAILURE() << "Expected 1 pending request"; + return net::ERR_FAILED; + } + network::TestURLLoaderFactory::PendingRequest* pending_request = + test_url_loader_factory_.GetPendingRequest(0); + if (!pending_request) { + ADD_FAILURE() << "Pending request is null"; + return net::ERR_FAILED; + } + + net::RedirectInfo redirect_info; + redirect_info.status_code = 302; + redirect_info.new_url = redirect_url; + redirect_info.new_method = "GET"; + + network::mojom::URLResponseHeadPtr response_head = + network::mojom::URLResponseHead::New(); + + pending_request->client->OnReceiveRedirect(redirect_info, + std::move(response_head)); + + client.RunUntilRedirectReceived(); + EXPECT_TRUE(client.has_received_redirect()); + EXPECT_EQ(redirect_url, client.redirect_info().new_url); + + network::HttpRequestHeadersUpdateParams update_params; + loader_remote->FollowRedirect(std::move(update_params), redirect_url); + + bool condition_met = base::test::RunUntil([&]() { + return client.has_received_completion() || + (pending_request->test_url_loader && + !pending_request->test_url_loader->follow_redirect_params() + .empty()); + }); + EXPECT_TRUE(condition_met); + + if (pending_request->test_url_loader && + !pending_request->test_url_loader->follow_redirect_params().empty()) { + pending_request->client->OnReceiveResponse( + network::mojom::URLResponseHead::New(), + mojo::ScopedDataPipeConsumerHandle(), std::nullopt); + pending_request->client->OnComplete( + network::URLLoaderCompletionStatus(net::OK)); + } + + client.RunUntilComplete(); + return client.completion_status().error_code; + } + + content::BrowserTaskEnvironment task_environment_; + AwCookieAccessPolicy cookie_access_policy_; + network::TestURLLoaderFactory test_url_loader_factory_; +}; + +TEST_F(AwProxyingURLLoaderFactoryTest, BlocksUnsafeRedirectToContentUrl) { + EXPECT_EQ(net::ERR_UNSAFE_REDIRECT, + FollowRedirect(GURL("http://example.com"), + GURL("content://com.example.provider/file"))); +} +
Regression Test / PoC
diff --git a/android_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc b/android_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc
new file mode 100644
index 0000000..032caff
--- /dev/null
+++ b/android_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc
@@ -0,0 +1,150 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "android_webview/browser/network_service/aw_proxying_url_loader_factory.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "android_webview/browser/aw_origin_matched_header.h"
+#include "android_webview/browser/network_service/aw_browser_context_io_thread_handle.h"
+#include "base/functional/bind.h"
+#include "base/run_loop.h"
+#include "base/test/run_until.h"
+#include "base/test/scoped_feature_list.h"
+#include "content/public/test/browser_task_environment.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "net/base/net_errors.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "services/network/public/cpp/http_request_headers_update_params.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/url_loader_factory.mojom.h"
+#include "services/network/test/test_url_loader_client.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace android_webview {
+
+class AwProxyingURLLoaderFactoryTest : public testing::Test {
+ public:
+ AwProxyingURLLoaderFactoryTest()
+ : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP),
+ test_url_loader_factory_(true) {}
+
+ protected:
+ int FollowRedirect(const GURL& original_url, const GURL& redirect_url) {
+ mojo::Remote<network::mojom::URLLoaderFactory> proxy_factory_remote;
+ mojo::Receiver<network::mojom::URLLoaderFactory> target_factory_receiver(
+ &test_url_loader_factory_);
+
+ auto factory = std::make_unique<AwProxyingURLLoaderFactory>(
+ /*cookie_manager=*/std::nullopt, &cookie_access_policy_,
+ /*isolation_info=*/std::nullopt,
+ /*key=*/std::nullopt, content::FrameTreeNodeId(),
+ proxy_factory_remote.BindNewPipeAndPassReceiver(),
+ target_factory_receiver.BindNewPipeAndPassRemote(),
+ /*intercept_only=*/false,
+ /*security_options=*/std::nullopt,
+ /*origin_matched_headers=*/
+ std::vector<scoped_refptr<AwOriginMatchedHeader>>(),
+ /*browser_context_handle=*/nullptr,
+ /*navigation_id=*/std::nullopt);
+
+ network::ResourceRequest request;
+ request.url = original_url;
+ request.method = "GET";
+
+ mojo::Remote<network::mojom::URLLoader> loader_remote;
+ network::TestURLLoaderClient client;
+
+ proxy_factory_remote->CreateLoaderAndStart(
+ loader_remote.BindNewPipeAndPassReceiver(),
+ /*request_id=*/1,
+ /*options=*/0, request, client.CreateRemote(),
+ net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+
+ if (test_url_loader_factory_.NumPending() != 1) {
+ ADD_FAILURE() << "Expected 1 pending request";
+ return net::ERR_FAILED;
+ }
+ network::TestURLLoaderFactory::PendingRequest* pending_request =
+ test_url_loader_factory_.GetPendingRequest(0);
+ if (!pending_request) {
+ ADD_FAILURE() << "Pending request is null";
+ return net::ERR_FAILED;
+ }
+
+ net::RedirectInfo redirect_info;
+ redirect_info.status_code = 302;
+ redirect_info.new_url = redirect_url;
+ redirect_info.new_method = "GET";
+
+ network::mojom::URLResponseHeadPtr response_head =
+ network::mojom::URLResponseHead::New();
+
+ pending_request->client->OnReceiveRedirect(redirect_info,
+ std::move(response_head));
+
+ client.RunUntilRedirectReceived();
+ EXPECT_TRUE(client.has_received_redirect());
+ EXPECT_EQ(redirect_url, client.redirect_info().new_url);
+
+ network::HttpRequestHeadersUpdateParams update_params;
+ loader_remote->FollowRedirect(std::move(update_params), redirect_url);
+
+ bool condition_met = base::test::RunUntil([&]() {
+ return client.has_received_completion() ||
+ (pending_request->test_url_loader &&
+ !pending_request->test_url_loader->follow_redirect_params()
+ .empty());
+ });
+ EXPECT_TRUE(condition_met);
+
+ if (pending_request->test_url_loader &&
+ !pending_request->test_url_loader->follow_redirect_params().empty()) {
+ pending_request->client->OnReceiveResponse(
+ network::mojom::URLResponseHead::New(),
+ mojo::ScopedDataPipeConsumerHandle(), std::nullopt);
+ pending_request->client->OnComplete(
+ network::URLLoaderCompletionStatus(net::OK));
+ }
+
+ client.RunUntilComplete();
+ return client.completion_status().error_code;
+ }
+
+ content::BrowserTaskEnvironment task_environment_;
+ AwCookieAccessPolicy cookie_access_policy_;
+ network::TestURLLoaderFactory test_url_loader_factory_;
+};
+
+TEST_F(AwProxyingURLLoaderFactoryTest, BlocksUnsafeRedirectToContentUrl) {
+ EXPECT_EQ(net::ERR_UNSAFE_REDIRECT,
+ FollowRedirect(GURL("http://example.com"),
+ GURL("content://com.example.provider/file")));
+}
+
+TEST_F(AwProxyingURLLoaderFactoryTest, BlocksUnsafeRedirectToFile) {
+ EXPECT_EQ(net::ERR_UNSAFE_REDIRECT,
+ FollowRedirect(GURL("http://example.com"),
+ GURL("file:///android_asset/test.html")));
+}
+
+TEST_F(AwProxyingURLLoaderFactoryTest, AllowsSafeRedirectToFile) {
+ EXPECT_EQ(net::OK,
+ FollowRedirect(GURL("file:///foo/bar"), GURL("file:///foo/baz")));
+}
+
+TEST_F(AwProxyingURLLoaderFactoryTest, AllowsSafeRedirect) {
+ EXPECT_EQ(net::OK, FollowRedirect(GURL("http://example.com"),
+ GURL("http://otherexample.com")));
+}
+
+} // namespace android_webview
diff --git a/android_webview/test/BUILD.gn b/android_webview/test/BUILD.gn
index a30bbef10..e3fdf26a 100644
--- a/android_webview/test/BUILD.gn
+++ b/android_webview/test/BUILD.gn
@@ -870,6 +870,7 @@
"../browser/metrics/aw_server_side_allowlist_metrics_provider_unittest.cc",
"../browser/metrics/visibility_metrics_logger_unittest.cc",
"../browser/network_service/aw_proxying_restricted_cookie_manager_unittest.cc",
+ "../browser/network_service/aw_proxying_url_loader_factory_unittest.cc",
"../browser/permission/media_access_permission_request_unittest.cc",
"../browser/permission/permission_request_handler_unittest.cc",
"../browser/prefetch/aw_prefetch_manager_unittest.cc",
Original Bug Report
Potential Android WebView Sandbox Escape via Redirect and Mojo Double-Bind
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 security team.
Overview: A compromised renderer in Android WebView can potentially read sensitive host-app data by exploiting an insecure redirect handling mechanism. Missing scheme validation in the network stack combined with a Mojo receiver double-bind in WebView’s proxy loader allows an attacker to bypass security checks and access local content:// URIs.
Affected files:
android_webview/browser/network_service/aw_proxying_url_loader_factory.ccservices/network/cors/cors_url_loader.ccandroid_webview/browser/network_service/net_helpers.cc
Estimated timestamp from git blame: 2024-06-28
Description
A potential vulnerability exists in Android WebView’s request interception logic that could allow a compromised renderer process to read data from any ContentProvider the host application has permission to access. This includes sensitive information such as contacts, SMS, and private app data.
The issue arises from a combination of missing scheme validation for redirects in the Network Service and a Mojo receiver double-bind bug in AwProxyingURLLoaderFactory.
Root Cause Analysis
- Insecure Redirect Handling: When an HTTP subresource fetch (e.g.,
kNoCors) receives a 302 redirect to a local URI likecontent://com.android.contacts/data/1, the Network Service evaluates the redirect.URLRequestJobFactory::IsSafeRedirectTargetdefaults totruefor unregistered schemes.CorsURLLoader::OnReceiveRedirectrelies on this and forwards the redirect to the renderer without explicitly blockingcontent://. - Renderer Check Bypass: A compromised renderer can bypass Blink’s
SecurityOrigin::CanDisplaychecks (which normally block local redirects) and send aFollowRedirectIPC directly to the browser. - Mojo Double-Bind in WebView Proxy: The
FollowRedirectIPC reachesInterceptedRequest::FollowRedirectinAwProxyingURLLoaderFactory. It updates the target URL to thecontent://URI and callsRestart(), which synchronously reachesContinueAfterIntercept(). - Silent Pipe Replacement:
ContinueAfterIntercept()detects thecontent://scheme and creates anAndroidStreamReaderURLLoaderto read the local data. To connect this new loader, it callsproxied_client_receiver_.BindNewPipeAndPassRemote(). Because the receiver is already bound to the active network loader, this violates Mojo invariants. However, in Release builds, theDCHECK(!is_bound())is compiled out. Mojo silently drops the old pipe (orphaning the network loader and suppressing its subsequentnet::ERR_UNKNOWN_URL_SCHEMEerror) and binds the new local loader. - Sandbox Escape: The
AndroidStreamReaderURLLoadersuccessfully reads the sensitivecontent://URI using the host app’s elevated permissions and streams the data back to the compromised renderer.
Potential Reproduction Steps
Note: These are suggested/potential steps to trigger the vulnerability. Our tooling agent does not currently have the ability to run code or verify a working proof of concept.
- Gain code execution in an Android WebView renderer process.
- Initiate a subresource fetch (e.g., via
fetch()withmode: 'no-cors') to an attacker-controlled HTTP server. - Have the attacker-controlled server respond with a
302 Foundand aLocationheader pointing to a sensitive local URI (e.g.,Location: content://com.android.contacts/data/1). - Upon receiving the redirect IPC in the compromised renderer, ignore Blink-side security checks and manually send a
FollowRedirectMojo IPC back to the browser process. - The browser process will inadvertently re-bind the data pipe to a local
AndroidStreamReaderURLLoader. - Read the sensitive host application data from the response body data pipe.
Suggested Fix
- Fix the Mojo Double-Bind: In
InterceptedRequest::ContinueAfterIntercept()(withinaw_proxying_url_loader_factory.cc), check ifproxied_client_receiver_.is_bound()before attempting to bind a new pipe. If it is already bound (e.g., during a redirect to a local scheme), the request should be explicitly aborted or reset properly. - Block Local Redirects: Explicitly block redirects from external schemes (HTTP/HTTPS) to local WebView schemes (
content://,file:///android_asset/, etc.) withinInterceptedRequest::FollowRedirectorCorsURLLoader::CheckRedirectLocation.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.