Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactExternally controlled reference in WebView
DescriptionExternally controlled reference in WebView
ComponentWebView
Bug ClassLogic Error
Tracker499007248
Fix commit751df049a7db (chromium/src) +162/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
android_webview/browser/network_service/aw_proxying_url_loader_factory.cc
modified
AwProxyingURLLoaderFactoryTest
android_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
if
android_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc
modified
TEST_F
android_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc
modified

Files Changed

  • android_webview/browser/network_service/aw_proxying_url_loader_factory.cc
  • android_webview/browser/network_service/aw_proxying_url_loader_factory_unittest.cc
From 751df049a7db402799a586c0d1fa92a64fe5be68 Mon Sep 17 00:00:00 2001
From: Joanne de Abreu <jdeabreu@google.com>
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 <ntfschr@chromium.org>
Commit-Queue: Joanne de Abreu <jdeabreu@google.com>
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")));
+}
+
Loading diff…

Regression Test / PoC

shipped with the fix
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",
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • services/network/cors/cors_url_loader.cc
  • android_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

  1. Insecure Redirect Handling: When an HTTP subresource fetch (e.g., kNoCors) receives a 302 redirect to a local URI like content://com.android.contacts/data/1, the Network Service evaluates the redirect. URLRequestJobFactory::IsSafeRedirectTarget defaults to true for unregistered schemes. CorsURLLoader::OnReceiveRedirect relies on this and forwards the redirect to the renderer without explicitly blocking content://.
  2. Renderer Check Bypass: A compromised renderer can bypass Blink’s SecurityOrigin::CanDisplay checks (which normally block local redirects) and send a FollowRedirect IPC directly to the browser.
  3. Mojo Double-Bind in WebView Proxy: The FollowRedirect IPC reaches InterceptedRequest::FollowRedirect in AwProxyingURLLoaderFactory. It updates the target URL to the content:// URI and calls Restart(), which synchronously reaches ContinueAfterIntercept().
  4. Silent Pipe Replacement: ContinueAfterIntercept() detects the content:// scheme and creates an AndroidStreamReaderURLLoader to read the local data. To connect this new loader, it calls proxied_client_receiver_.BindNewPipeAndPassRemote(). Because the receiver is already bound to the active network loader, this violates Mojo invariants. However, in Release builds, the DCHECK(!is_bound()) is compiled out. Mojo silently drops the old pipe (orphaning the network loader and suppressing its subsequent net::ERR_UNKNOWN_URL_SCHEME error) and binds the new local loader.
  5. Sandbox Escape: The AndroidStreamReaderURLLoader successfully reads the sensitive content:// 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.

  1. Gain code execution in an Android WebView renderer process.
  2. Initiate a subresource fetch (e.g., via fetch() with mode: 'no-cors') to an attacker-controlled HTTP server.
  3. Have the attacker-controlled server respond with a 302 Found and a Location header pointing to a sensitive local URI (e.g., Location: content://com.android.contacts/data/1).
  4. Upon receiving the redirect IPC in the compromised renderer, ignore Blink-side security checks and manually send a FollowRedirect Mojo IPC back to the browser process.
  5. The browser process will inadvertently re-bind the data pipe to a local AndroidStreamReaderURLLoader.
  6. Read the sensitive host application data from the response body data pipe.

Suggested Fix

  1. Fix the Mojo Double-Bind: In InterceptedRequest::ContinueAfterIntercept() (within aw_proxying_url_loader_factory.cc), check if proxied_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.
  2. Block Local Redirects: Explicitly block redirects from external schemes (HTTP/HTTPS) to local WebView schemes (content://, file:///android_asset/, etc.) within InterceptedRequest::FollowRedirect or CorsURLLoader::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.

View on issue tracker