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 --- 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 @@ -101,9 +101,13 @@ class MockRestrictedCookieManager bool apply_devtools_overrides, bool force_disable_third_party_cookies, GetCookiesStringCallback callback) override { + get_cookies_string_called_ = true; + last_force_disable_3pcs_ = force_disable_third_party_cookies; + last_site_for_cookies_ = site_for_cookies; last_get_cookies_shared_memory_param_ = get_version_shared_memory; std::move(callback).Run(network::mojom::kInvalidCookieVersion, - base::ReadOnlySharedMemoryRegion(), ""); + base::ReadOnlySharedMemoryRegion(), + "tracker_id=secret"); } void CookiesEnabledFor(const GURL& url, @@ -121,10 +125,18 @@ class MockRestrictedCookieManager bool last_set_cookie_shared_memory_param() const { return last_set_cookie_shared_memory_param_; } + bool get_cookies_string_called() const { return get_cookies_string_called_; } + bool last_force_disable_3pcs() const { return last_force_disable_3pcs_; } + const net::SiteForCookies& last_site_for_cookies() const { + return last_site_for_cookies_; + } private: bool last_get_cookies_shared_memory_param_ = false; bool last_set_cookie_shared_memory_param_ = false; + bool get_cookies_string_called_ = false; + bool last_force_disable_3pcs_ = false; + net::SiteForCookies last_site_for_cookies_; }; class AwProxyingRestrictedCookieManagerTest : public testing::Test { @@ -415,6 +427,101 @@ TEST_F(AwProxyingRestrictedCookieManagerTest, EXPECT_FALSE(mock_rcm.last_set_cookie_shared_memory_param()); } +// --------------------------------------------------------------------------- +// SECURITY: Demonstrates that AwProxyingRestrictedCookieManager trusts the +// renderer-supplied `site_for_cookies` Mojo parameter when enforcing the +// per-WebView setAcceptThirdPartyCookies(false) policy. +// +// Scenario: tracker.example is embedded as a cross-site iframe inside +// victim.example. The host app has disabled third-party cookies. The proxy is +// bound for origin=tracker.example with the *trusted* +// isolation_info.site_for_cookies = victim.example, but that trusted value is +// discarded at CreateAndBind() time. A compromised renderer sends +// site_for_cookies = tracker.example instead, and the proxy treats the request +// as first-party. +// --------------------------------------------------------------------------- +TEST_F(AwProxyingRestrictedCookieManagerTest, + RendererSpoofedSiteForCookiesBypassesThirdPartyBlock) { + base::test::ScopedFeatureList feature_list; + feature_list.InitAndEnableFeature(features::kWebViewLatchedCookiePolicy); + + // App config: cookies on, third-party cookies OFF. + cookie_access_policy_.SetShouldAcceptCookies(true); + // (process_id=0, frame_id=0) has no AwContentsIoThreadClient, so + // GetShouldAcceptThirdPartyCookies() latches to false — equivalent to + // CookieManager.setAcceptThirdPartyCookies(webview, false). + + MockRestrictedCookieManager mock_rcm; + mojo::Receiver mock_receiver( + &mock_rcm); + mojo::Remote proxy_remote; + + CreateProxyOnIOThread(mock_receiver.BindNewPipeAndPassRemote(), + proxy_remote.BindNewPipeAndPassReceiver()); + ASSERT_TRUE( + base::test::RunUntil([&]() { return proxy_remote.is_connected(); })); + + const GURL kTracker("https://tracker.example/"); + const GURL kVictim("https://victim.example/"); + + // --- Baseline: honest renderer in a cross-site iframe --- + // site_for_cookies reflects the real top-level site (victim.example). + { + base::RunLoop run_loop; + std::string result = ""; + proxy_remote->GetCookiesString( + kTracker, net::SiteForCookies::FromUrl(kVictim), + url::Origin::Create(kVictim), net::StorageAccessApiStatus::kNone, + /*get_version_shared_memory=*/false, + /*is_ad_tagged=*/false, + /*apply_devtools_overrides=*/false, + /*force_disable_third_party_cookies=*/false, + base::BindOnce( + [](base::RunLoop* loop, std::string* out, uint64_t, + base::ReadOnlySharedMemoryRegion, const std::string& s) { + *out = s; + loop->Quit(); + }, + &run_loop, &result)); + run_loop.Run(); + // Honest path: proxy classifies as third-party -> forwards with + // force_disable_third_party_cookies=true (PartitionedStateAllowedOnly). + ASSERT_TRUE(mock_rcm.get_cookies_string_called()); + EXPECT_TRUE(mock_rcm.last_force_disable_3pcs()) + << "Honest cross-site request should have 3PCs disabled"; + } + + // --- Attack: compromised renderer spoofs site_for_cookies --- + // The renderer lies that site_for_cookies == its own origin. + { + base::RunLoop run_loop; + std::string result = ""; + proxy_remote->GetCookiesString( + kTracker, + /*SPOOFED*/ net::SiteForCookies::FromUrl(kTracker), + url::Origin::Create(kTracker), net::StorageAccessApiStatus::kNone, + /*get_version_shared_memory=*/false, + /*is_ad_tagged=*/false, + /*apply_devtools_overrides=*/false, + /*force_disable_third_party_cookies=*/false, + base::BindOnce( + [](base::RunLoop* loop, std::string* out, uint64_t, + base::ReadOnlySharedMemoryRegion, const std::string& s) { + *out = s; + loop->Quit(); + }, + &run_loop, &result)); + run_loop.Run(); + // BUG: the proxy trusted the spoofed value -> kStateAllowed -> + // forwarded WITHOUT force_disable_third_party_cookies. Unpartitioned + // cookies are returned despite setAcceptThirdPartyCookies(false). + EXPECT_FALSE(mock_rcm.last_force_disable_3pcs()) + << "Spoofed site_for_cookies bypassed the WebView 3PC block"; + EXPECT_EQ(result, "tracker_id=secret"); + } +} + } // namespace } // namespace android_webview