CVE-2026-13924
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifandroid_webview/browser/network_service/aw_proxying_restricted_cookie_manager.cc |
modified |
Files Changed
android_webview/browser/aw_content_browser_client.ccandroid_webview/browser/network_service/aw_proxying_restricted_cookie_manager.cc
Patch
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;
Regression Test / PoC
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_;
Original Bug Report
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.ccandroid_webview/browser/aw_content_browser_client.ccandroid_webview/browser/aw_cookie_access_policy.ccservices/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:
- Bypass the
setAcceptThirdPartyCookies(false)policy configured by the Android application developer, enabling cross-site tracking. - Bypass
SameSitecookie protections, allowing a compromised third-party frame to read and writeSameSite=LaxandSameSite=Strictcookies for its own origin.
Potential Exploitation Steps
Note: These are suggested steps based on static analysis.
- An Android application configures a WebView and calls
CookieManager.setAcceptThirdPartyCookies(webview, false). - The WebView loads
https://victim.example, which embeds a third-party iframe tohttps://tracker.example. - An attacker achieves Remote Code Execution (RCE) in the sandboxed renderer process for the
tracker.exampleiframe. - From the compromised renderer, the attacker crafts a Mojo IPC to
RestrictedCookieManager::GetCookiesString. - The attacker sets
url=https://tracker.exampleand spoofssite_for_cookies=https://tracker.example. - The browser process proxy (
AwProxyingRestrictedCookieManager) evaluates the spoofedsite_for_cookies, determines it is a first-party request, and passesdisable_3pcs = falseto the network service. - The network service logs an error about the
site_for_cookiesmismatch but continues execution. It uses the spoofed value to compute aSAME_SITE_STRICTcontext. - 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
- Proxy Enforcement:
AwProxyingRestrictedCookieManagershould not trust thesite_for_cookiesparameter provided by the renderer over Mojo.AwContentBrowserClient::WillCreateRestrictedCookieManagerreceives the trustedIsolationInfo. It should pass the trustedsite_for_cookiesfrom theIsolationInfoto the constructor ofAwProxyingRestrictedCookieManager, and the proxy should use this trusted value for all policy evaluations instead of the renderer-supplied arguments. - Network Service Enforcement: Address the TODO in
RestrictedCookieManager::ValidateAccessToCookiesAt(crbug.com/402207912) to returnfalseand trigger aReportBadMessagewhen the renderer’ssite_for_cookiesdoes not match theBoundSiteForCookies(), 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.