Medium watch CVSS 8.3 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactExternally controlled reference in WebView
DescriptionExternally controlled reference in WebView
ComponentWebView
Bug ClassLogic 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.
Tracker499007248
Fix commit751df049a7db (chromium/src) +162/-0
CWECWE-610
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-02 (per vendor advisory)
Fix landed2026-07-08 (public git)
Disclosed2026-08-25
Patch gap48 days — fix public in source before the advisory

Background

`InterceptedRequest`
the per-request object in Android WebView’s proxying URL loader that mediates a navigation/resource load between the renderer’s client and the real network loader.
`FollowRedirect`
the mojo-exposed method the renderer calls to continue a load after a redirect, optionally supplying a new_url to follow.
`content::IsSafeRedirectTarget()`
a helper that decides whether redirecting from one URL to another is permitted (e.g. blocking http(s) → content:///file://).
`content://` / `file://` URLs
local Android resource schemes that expose app content providers and local files, which web-origin pages must not be able to reach via redirect.

Root Cause Analysis

Android WebView relied on a renderer-side check to stop an http(s) load from redirecting to a content:// or file:// URL, but the browser-side proxying loader (InterceptedRequest) performed no equivalent validation when the renderer invoked FollowRedirect. A compromised renderer could therefore bypass the renderer check and drive a redirect whose target is a local content/file URL, causing the browser to follow it. The violated invariant is that redirect targets crossing into local schemes must be authorized on the browser side, which is the only trustworthy boundary against a compromised renderer.

The fix tracks the pre-redirect URL in a new last_url_ field — initialized to request.url in the constructor and updated to request_.url inside OnReceiveRedirect before request_.url is overwritten with the new target — and then, in FollowRedirect, computes target_url = new_url.value_or(request_.url) and, when request_was_redirected_ is set, rejects the redirect via SendErrorAndCompleteImmediately(net::ERR_UNSAFE_REDIRECT) (after target_loader_.reset()) unless content::IsSafeRedirectTarget(last_url_, target_url) allows it. This adds the missing browser-side enforcement.

Key insight
The core mistake was enforcing the redirect-to-local-scheme restriction only in the renderer, an untrusted process; the fix adds a browser-side IsSafeRedirectTarget(last_url_, target_url) gate in InterceptedRequest::FollowRedirect so a compromised renderer can no longer follow an unsafe redirect.

Attack Path

  1. Compromise the renderer The attacker gains code execution in a WebView renderer process (inferred; a compromised renderer is the stated threat model).
  2. Start a web-origin load A request begins for an http(s) URL, recorded as last_url_.
  3. Receive a redirect The server (or the renderer) drives a redirect; OnReceiveRedirect runs, updating last_url_ and setting request_was_redirected_.
  4. Call `FollowRedirect` with a local target The compromised renderer invokes FollowRedirect with a new_url of content://… or file:///android_asset/…, bypassing the renderer-side check that would normally block it.
  5. Load local content (pre-fix) Without the browser-side check, WebView follows the redirect into the local scheme, exposing local files or content-provider data to the web origin; post-fix this is rejected with net::ERR_UNSAFE_REDIRECT.

Impact Assessment

A compromised renderer can cause Android WebView to follow a redirect from a web origin into a local content:// or file:// URL, giving the attacker access to local files / content-provider data that the web origin should never reach — an externally controlled reference / local-resource disclosure enforced in the browser process. The precondition is a renderer already compromised or otherwise able to bypass the renderer-side redirect check. The advisory classifies this as a logic error (externally controlled reference) in WebView; no in-the-wild exploitation is stated, and the initial renderer compromise is (inferred).

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_unittest.cc
  • android_webview/browser/network_service/aw_proxying_url_loader_factory.cc
  • android_webview/test/BUILD.gn

Audit Directions

  • Renderer-only security checks
    Look for WebView/loader restrictions enforced solely in the renderer with no matching browser-side enforcement, which a compromised renderer can bypass.
  • Redirect scheme transitions
    Audit redirect-following code paths for validation that a web-origin load cannot transition into local schemes (content://, file://) or other privileged schemes.
  • State captured across redirects
    Review loaders that must compare pre- and post-redirect URLs to ensure the source URL is captured before request_.url is mutated, avoiding checks against the already-updated value.
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")));
+}
+
+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…

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 [email protected]

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