Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in WebView
DescriptionInsufficient validation of untrusted input in WebView
ComponentWebView
Bug ClassLogic Error
Tracker511784747
Fix commit672e7780862d (chromium/src) +52/-38
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
android_webview/browser/network_service/aw_proxying_restricted_cookie_manager.cc
modified

Files Changed

  • android_webview/browser/aw_content_browser_client.cc
  • android_webview/browser/network_service/aw_proxying_restricted_cookie_manager.cc
From 672e7780862d1dd728f3643f902341e8c95f60fb Mon Sep 17 00:00:00 2001
From: Ben Wiser <[email protected]>
Date: Thu, 14 May 2026 09:01:13 -0700
Subject: [PATCH] webview: Fix proxying restricted cookie manager trusting render process

AwProxyingRestrictedCookieManager incorrectly trusted the renderer-
supplied site_for_cookies parameter to enforce WebView's third-party
cookie policy. A compromised renderer could spoof this parameter to
bypass setAcceptThirdPartyCookies(false) and additionally bypass
SameSite cookie protections.

I've updated this to instead rely on the site isolation info coming from
the browser process when we create the proxying restricted cookie
manager. It is important to note that the proxying cookie manager lives
in the browser. This is used for requests from javascript.

The most important impact of this is around partitioned cookies since
they rely on the behaviour of how our SiteForCookies operates in order
to still be enabled when 3PCs are disabled. If those tests are passing I
trust this change.

Test: Ran webview unit and instrumentation tests.
Bug: 511784747
Change-Id: Iccc70276ad21c51727056e0072a37beb1affbf24
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7831207
Reviewed-by: Nate Fischer <[email protected]>
Reviewed-by: Dylan Cutler <[email protected]>
Commit-Queue: Rupert Wiser <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1630654}
---

diff --git a/android_webview/browser/aw_content_browser_client.cc b/android_webview/browser/aw_content_browser_client.cc
index bf698915..556d32d 100644
--- a/android_webview/browser/aw_content_browser_client.cc
+++ b/android_webview/browser/aw_content_browser_client.cc
@@ -1313,7 +1313,8 @@
 
   AwProxyingRestrictedCookieManager::CreateAndBind(
       std::move(target_rcm_remote), is_service_worker, process_id, routing_id,
-      std::move(orig_receiver), aw_cookie_access_policy);
+      isolation_info.site_for_cookies(), std::move(orig_receiver),
+      aw_cookie_access_policy);
 
   return false;  // only made a proxy, still need the actual impl to be made.
 }
diff --git a/android_webview/browser/network_service/aw_proxying_restricted_cookie_manager.cc b/android_webview/browser/network_service/aw_proxying_restricted_cookie_manager.cc
index 53626f9..44996808 100644
--- a/android_webview/browser/network_service/aw_proxying_restricted_cookie_manager.cc
+++ b/android_webview/browser/network_service/aw_proxying_restricted_cookie_manager.cc
@@ -54,7 +54,7 @@
   void OnCookieChange(const net::CookieChangeInfo& change) override {
     if (aw_restricted_cookie_manager_) {
       PrivacySetting cookieState =
-          aw_restricted_cookie_manager_->AllowCookies(url_, site_for_cookies_);
+          aw_restricted_cookie_manager_->AllowCookies(url_);
 
       if (cookieState == PrivacySetting::kStateAllowed ||
           (cookieState == PrivacySetting::kPartitionedStateAllowedOnly &&
@@ -78,6 +78,7 @@
     bool is_service_worker,
     int process_id,
     int frame_id,
+    const net::SiteForCookies& site_for_cookies,
     mojo::PendingReceiver<network::mojom::RestrictedCookieManager> receiver,
     AwCookieAccessPolicy* aw_cookie_access_policy) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
@@ -94,7 +95,7 @@
       base::BindOnce(
           &AwProxyingRestrictedCookieManager::CreateAndBindOnIoThread,
           std::move(underlying_rcm), is_service_worker, frame_token,
-          std::move(receiver), aw_cookie_access_policy));
+          site_for_cookies, std::move(receiver), aw_cookie_access_policy));
 }
 
 AwProxyingRestrictedCookieManager::~AwProxyingRestrictedCookieManager() {
@@ -103,7 +104,7 @@
 
 void AwProxyingRestrictedCookieManager::GetAllForUrl(
     const GURL& url,
-    const net::SiteForCookies& site_for_cookies,
+    const net::SiteForCookies& /*site_for_cookies*/,
     const url::Origin& top_frame_origin,
     net::StorageAccessApiStatus storage_access_api_status,
     network::mojom::CookieManagerGetOptionsPtr options,
@@ -113,7 +114,7 @@
     GetAllForUrlCallback callback) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
 
-  PrivacySetting cookieState = AllowCookies(url, site_for_cookies);
+  PrivacySetting cookieState = AllowCookies(url);
 
   if (cookieState == PrivacySetting::kStateDisallowed) {
     std::move(callback).Run(std::vector<net::CookieWithAccessResult>());
@@ -125,7 +126,7 @@
       cookieState == PrivacySetting::kPartitionedStateAllowedOnly;
 
   underlying_restricted_cookie_manager_->GetAllForUrl(
-      url, site_for_cookies, top_frame_origin, storage_access_api_status,
+      url, site_for_cookies_, top_frame_origin, storage_access_api_status,
       std::move(options), is_ad_tagged, apply_devtools_overrides, disable_3pcs,
       std::move(callback));
 }
@@ -133,14 +134,14 @@
 void AwProxyingRestrictedCookieManager::SetCanonicalCookie(
     network::mojom::RestrictedCanonicalCookieParamsPtr cookie_params,
     const GURL& url,
-    const net::SiteForCookies& site_for_cookies,
+    const net::SiteForCookies& /*site_for_cookies*/,
     const url::Origin& top_frame_origin,
     net::StorageAccessApiStatus storage_access_api_status,
     bool is_ad_tagged,
     bool apply_devtools_overrides,
     SetCanonicalCookieCallback callback) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
-  PrivacySetting cookieState = AllowCookies(url, site_for_cookies);
+  PrivacySetting cookieState = AllowCookies(url);
 
   if (cookieState == PrivacySetting::kStateDisallowed) {
     std::move(callback).Run(false);
@@ -151,7 +152,7 @@
           network::mojom::RestrictedCookiePartition::PARTITIONED ||
       cookieState == PrivacySetting::kStateAllowed) {
     underlying_restricted_cookie_manager_->SetCanonicalCookie(
-        std::move(cookie_params), url, site_for_cookies, top_frame_origin,
+        std::move(cookie_params), url, site_for_cookies_, top_frame_origin,
         storage_access_api_status, is_ad_tagged, apply_devtools_overrides,
         std::move(callback));
   } else {
@@ -161,7 +162,7 @@
 
 void AwProxyingRestrictedCookieManager::AddChangeListener(
     const GURL& url,
-    const net::SiteForCookies& site_for_cookies,
+    const net::SiteForCookies& /*site_for_cookies*/,
     const url::Origin& top_frame_origin,
     net::StorageAccessApiStatus storage_access_api_status,
     mojo::PendingRemote<network::mojom::CookieChangeListener> listener,
@@ -172,7 +173,7 @@
       proxy_listener_remote;
   auto proxy_listener =
       std::make_unique<AwProxyingRestrictedCookieManagerListener>(
-          url, site_for_cookies, weak_factory_.GetWeakPtr(),
+          url, site_for_cookies_, weak_factory_.GetWeakPtr(),
           std::move(listener));
 
   mojo::MakeSelfOwnedReceiver(
@@ -180,13 +181,13 @@
       proxy_listener_remote.InitWithNewPipeAndPassReceiver());
 
   underlying_restricted_cookie_manager_->AddChangeListener(
-      url, site_for_cookies, top_frame_origin, storage_access_api_status,
+      url, site_for_cookies_, top_frame_origin, storage_access_api_status,
       std::move(proxy_listener_remote), std::move(callback));
 }
 
 void AwProxyingRestrictedCookieManager::SetCookieFromString(
     const GURL& url,
-    const net::SiteForCookies& site_for_cookies,
+    const net::SiteForCookies& /*site_for_cookies*/,
     const url::Origin& top_frame_origin,
     net::StorageAccessApiStatus storage_access_api_status,
     bool is_ad_tagged,
@@ -194,7 +195,7 @@
     const std::string& cookie) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
 
-  PrivacySetting cookieState = AllowCookies(url, site_for_cookies);
+  PrivacySetting cookieState = AllowCookies(url);
 
   if (cookieState == PrivacySetting::kStateDisallowed) {
     return;
@@ -209,14 +210,14 @@
       (parsed_cookie.IsValid() && parsed_cookie.IsPartitioned() &&
        parsed_cookie.IsSecure())) {
     underlying_restricted_cookie_manager_->SetCookieFromString(
-        url, site_for_cookies, top_frame_origin, storage_access_api_status,
+        url, site_for_cookies_, top_frame_origin, storage_access_api_status,
         is_ad_tagged, apply_devtools_overrides, cookie);
   }
 }
 
 void AwProxyingRestrictedCookieManager::GetCookiesString(
     const GURL& url,
-    const net::SiteForCookies& site_for_cookies,
+    const net::SiteForCookies& /*site_for_cookies*/,
     const url::Origin& top_frame_origin,
     net::StorageAccessApiStatus storage_access_api_status,
     bool get_version_shared_memory,
@@ -226,7 +227,7 @@
     GetCookiesStringCallback callback) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
 
-  PrivacySetting cookieState = AllowCookies(url, site_for_cookies);
+  PrivacySetting cookieState = AllowCookies(url);
 
   if (cookieState == PrivacySetting::kStateDisallowed) {
     std::move(callback).Run(network::mojom::kInvalidCookieVersion,
@@ -248,21 +249,20 @@
       get_version_shared_memory;
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/android_webview/browser/network_service/aw_proxying_restricted_cookie_manager_unittest.cc b/android_webview/browser/network_service/aw_proxying_restricted_cookie_manager_unittest.cc
index a30a2d1..48399f8 100644
--- a/android_webview/browser/network_service/aw_proxying_restricted_cookie_manager_unittest.cc
+++ b/android_webview/browser/network_service/aw_proxying_restricted_cookie_manager_unittest.cc
@@ -131,12 +131,14 @@
  protected:
   void CreateProxyOnIOThread(
       mojo::PendingRemote<network::mojom::RestrictedCookieManager> underlying,
-      mojo::PendingReceiver<network::mojom::RestrictedCookieManager> receiver) {
+      mojo::PendingReceiver<network::mojom::RestrictedCookieManager> receiver,
+      const net::SiteForCookies& site_for_cookies = net::SiteForCookies()) {
     AwProxyingRestrictedCookieManager::CreateAndBind(
         std::move(underlying),
         /*is_service_worker=*/false,
         /*process_id=*/0,
-        /*frame_id=*/0, std::move(receiver), &cookie_access_policy_);
+        /*frame_id=*/0, site_for_cookies, std::move(receiver),
+        &cookie_access_policy_);
   }
 
   content::BrowserTaskEnvironment task_environment_;
Loading diff…

Original Bug Report

reported by [email protected]

Potential bypass of WebView third-party cookie policy and SameSite protections via spoofed site_for_cookies

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 Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: AwProxyingRestrictedCookieManager incorrectly trusts the renderer-supplied site_for_cookies parameter to enforce WebView’s third-party cookie policy. A compromised renderer can spoof this parameter to bypass setAcceptThirdPartyCookies(false) and additionally bypass SameSite cookie protections.

Affected files:

  • android_webview/browser/network_service/aw_proxying_restricted_cookie_manager.cc
  • android_webview/browser/aw_content_browser_client.cc
  • android_webview/browser/aw_cookie_access_policy.cc
  • services/network/restricted_cookie_manager.cc

Estimated timestamp from git blame: 2026-04-13

Description

In Android WebView, AwProxyingRestrictedCookieManager (running in the browser process) acts as a proxy for RestrictedCookieManager (running in the network process). Its purpose is to enforce WebView-specific cookie policies, primarily the per-WebView setAcceptThirdPartyCookies() API, as the network service itself is not globally configured to block third-party cookies for WebView.

However, AwProxyingRestrictedCookieManager incorrectly trusts renderer-supplied Mojo parameters, specifically site_for_cookies, when determining if a request should be treated as first-party or third-party. When a renderer invokes Mojo methods such as GetAllForUrl, SetCanonicalCookie, or GetCookiesString, it provides its own site_for_cookies value. The proxy passes this value to AllowCookies(), which consults AwCookieAccessPolicy::CanAccessCookies() to decide whether the access should be allowed or if third-party cookies should be disabled.

A compromised renderer running in a third-party context (e.g., https://tracker.example embedded within https://victim.example) can spoof site_for_cookies to match its own requested url. This causes the proxy’s IsFirstParty() check to evaluate to true, tricking the proxy into treating the request as first-party. The proxy then forwards the request to the underlying RestrictedCookieManager in the network process with disable_3pcs = false (or force_disable_third_party_cookies = false).

While the network process’s RestrictedCookieManager::ValidateAccessToCookiesAt detects that the renderer-supplied site_for_cookies does not match the securely bound IsolationInfo, it currently only logs an ERROR and proceeds with the request as long as the URL matches the bound origin (due to a known issue tracked in crbug.com/402207912).

Consequently, the network service processes the request using the attacker’s spoofed site_for_cookies. This not only bypasses the WebView-specific third-party cookie block but also incorrectly calculates the SameSite context in MakeOptionsForGet/MakeOptionsForSet. By making the spoofed site_for_cookies match the URL, the network service evaluates the context as SAME_SITE_STRICT, allowing the cross-site attacker to read and write SameSite=Lax and SameSite=Strict cookies.

Impact

This vulnerability allows a compromised renderer to:

  1. Bypass the setAcceptThirdPartyCookies(false) policy configured by the Android application developer, enabling cross-site tracking.
  2. Bypass SameSite cookie protections, allowing a compromised third-party frame to read and write SameSite=Lax and SameSite=Strict cookies for its own origin.

Potential Exploitation Steps

Note: These are suggested steps based on static analysis.

  1. An Android application configures a WebView and calls CookieManager.setAcceptThirdPartyCookies(webview, false).
  2. The WebView loads https://victim.example, which embeds a third-party iframe to https://tracker.example.
  3. An attacker achieves Remote Code Execution (RCE) in the sandboxed renderer process for the tracker.example iframe.
  4. From the compromised renderer, the attacker crafts a Mojo IPC to RestrictedCookieManager::GetCookiesString.
  5. The attacker sets url=https://tracker.example and spoofs site_for_cookies=https://tracker.example.
  6. The browser process proxy (AwProxyingRestrictedCookieManager) evaluates the spoofed site_for_cookies, determines it is a first-party request, and passes disable_3pcs = false to the network service.
  7. The network service logs an error about the site_for_cookies mismatch but continues execution. It uses the spoofed value to compute a SAME_SITE_STRICT context.
  8. The network service returns the cookies for tracker.example (including SameSite cookies), successfully bypassing both the third-party cookie block and SameSite restrictions.

Suggested Fix

  1. Proxy Enforcement: AwProxyingRestrictedCookieManager should not trust the site_for_cookies parameter provided by the renderer over Mojo. AwContentBrowserClient::WillCreateRestrictedCookieManager receives the trusted IsolationInfo. It should pass the trusted site_for_cookies from the IsolationInfo to the constructor of AwProxyingRestrictedCookieManager, and the proxy should use this trusted value for all policy evaluations instead of the renderer-supplied arguments.
  2. Network Service Enforcement: Address the TODO in RestrictedCookieManager::ValidateAccessToCookiesAt (crbug.com/402207912) to return false and trigger a ReportBadMessage when the renderer’s site_for_cookies does not match the BoundSiteForCookies(), rather than just logging an error.

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


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
Links in the report