Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactPolicy bypass in WebView
DescriptionPolicy bypass in WebView
ComponentWebView
Bug ClassLogic Error
Tracker502501810
Fix commit7b41029da17e (chromium/src) +158/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

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

Files Changed

  • android_webview/browser/network_service/aw_proxying_url_loader_factory.cc
  • android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldInterceptRequestTest.java
From 7b41029da17ef90b40eb86ecc56db0a6b37276f1 Mon Sep 17 00:00:00 2001
From: Peter Pakkenberg <[email protected]>
Date: Tue, 21 Apr 2026 05:27:47 -0700
Subject: [PATCH] Ensure WebView cookie intercept respects same-site

Removing the cookies from the request when intercept is skipped ensures
that they are set correctly by the network code at lower layers. This
prevents information leaks to the server if there is a bug in the cookie
selection in the intercept code path.

This CL does also restrict the cookie selection for the intercept path
to ensure that Same-Site restrictions are respected for cookies, since
the feature is meant to only provide the cookies that would be attached
to the request.
This CL does also restrict the cookie selection for the intercept path
to ensure that Same-Site restrictions are respected for cookies, since
the feature is meant to only provide the cookies that would be attached
to the request.

Fixed: 502501810
Change-Id: I18a1c4c50f042db0a676966f91cc949baf7102f5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7772847
Commit-Queue: Peter Birk Pakkenberg <[email protected]>
Auto-Submit: Peter Birk Pakkenberg <[email protected]>
Reviewed-by: Nate Fischer <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1618084}
---

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 d7d9d8c..651b7c2 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
@@ -58,6 +58,7 @@
 #include "net/base/schemeful_site.h"
 #include "net/cookies/canonical_cookie.h"
 #include "net/cookies/cookie_inclusion_status.h"
+#include "net/cookies/cookie_util.h"
 #include "net/http/http_request_headers.h"
 #include "net/http/http_util.h"
 #include "services/network/public/cpp/features.h"
@@ -433,14 +434,13 @@
 }
 
 void InterceptedRequest::InterceptWithCookieHeader(std::string cookie) {
-  if (cookie != "") {
-    request_.headers.SetHeader(net::HttpRequestHeaders::kCookie, cookie);
-  }
-
   std::unique_ptr<AwContentsIoThreadClient> io_thread_client =
       GetIoThreadClient();
-
   if (io_thread_client != nullptr) {
+    // Attach cookies if we are intercepting.
+    if (cookie != "") {
+      request_.headers.SetHeader(net::HttpRequestHeaders::kCookie, cookie);
+    }
     // TODO: verify the case when WebContents::RenderFrameDeleted is called
     // before network request is intercepted (i.e. if that's possible and
     // whether it can result in any issues).
@@ -495,6 +495,11 @@
 }
 
 void InterceptedRequest::ContinueAfterIntercept() {
+  // The Cookie header may have been set in `InterceptWithCookieHeader`. But in
+  // cases where the application chose to not intercept, we should remove it
+  // again, so we are sure that the correct header is then set by the lower
+  // network layers.
+  request_.headers.RemoveHeader(net::HttpRequestHeaders::kCookie);
   // For WebViewClassic compatibility this job can only accept URLs that can be
   // opened. URLs that cannot be opened should be resolved by the next handler.
   //
@@ -1121,7 +1126,27 @@
 
   auto isolation_info = GetIsolationInfo(request);
 
-  net::CookieOptions options = net::CookieOptions::MakeAllInclusive();
+  net::CookieOptions options;
+  options.set_include_httponly();
+  options.set_do_not_update_access_time();
+  if (request.resource_type ==
+          static_cast<int32_t>(blink::mojom::ResourceType::kMainFrame) ||
+      request.resource_type ==
+          static_cast<int32_t>(blink::mojom::ResourceType::kSubFrame)) {
+    options.set_same_site_cookie_context(
+        net::cookie_util::ComputeSameSiteContextForRequest(
+            request.method, request.navigation_redirect_chain,
+            request.site_for_cookies, request.request_initiator,
+            request.resource_type ==
+                static_cast<int32_t>(blink::mojom::ResourceType::kMainFrame),
+            /*force_ignore_site_for_cookies=*/false,
+            /*ignore_unsafe_method_for_same_site_lax=*/false));
+  } else {
+    options.set_same_site_cookie_context(
+        net::cookie_util::ComputeSameSiteContextForSubresource(
+            request.url, request.site_for_cookies,
+            /*force_ignore_site_for_cookies=*/false));
+  }
 
   PrivacySetting privacy_setting = cookie_access_policy_->CanAccessCookies(
       request.url, isolation_info.site_for_cookies(), is_3pc_allowed);
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldInterceptRequestTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldInterceptRequestTest.java
index 19b875f7..dbe5266 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldInterceptRequestTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldInterceptRequestTest.java
@@ -1521,6 +1521,133 @@
     @Test
     @SmallTest
     @Feature({"AndroidWebView", "Network"})
+    public void testInterceptedCookieHeaders_sameSiteStrictBlocked() throws Throwable {
+        // This test asserts that same-site=Strict cookies are not provided on a cross-site request
+        // to shouldInterceptRequest or the web server when setIncludeCookiesOnIntercept is enabled.
+        mActivityTestRule.getAwSettingsOnUiThread(mAwContents).setIncludeCookiesOnIntercept(true);
+
+        // setAcceptThirdPartyCookies is needed for the cross-site request to even consider cookies
+        // in WebView injection logic.
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> mAwContents.getSettings().setAcceptThirdPartyCookies(true));
+
+        var cookieManager = mAwContents.getBrowserContextForPublicApi().getCookieManager();
+        cookieManager.removeAllCookies();
+
+        final String resourcePath = "/resource.txt";
+        final List<Pair<String, String>> responseHeaders = new ArrayList<>();
+        // Allow CORS from some.origin.test so the fetch doesn't fail early.
+        responseHeaders.add(new Pair<>("Access-Control-Allow-Origin", "http://some.origin.test"));
+        final String resourceUrl =
+                mWebServer.setResponse(resourcePath, "resource data", responseHeaders);
+
+        // Set a SameSite=Strict cookie for the resource.
+        cookieManager.setCookie(resourceUrl, "session=SECRET; SameSite=Strict");
+
+        // Main page origin: http://some.origin.test (BASE_URL)
+        // fetch(resourceUrl, {credentials: 'omit'})
+        // credentials: 'omit' should definitely prevent cookies from being sent.
+        String fetchArgs = String.format("'%s', {credentials: 'omit'}", resourceUrl);
+        final Future<String> future = loadPageAndFetchInternal(null, fetchArgs);
+
+        Assert.assertEquals(
+                "fetch should succeed",
+                "cors",
+                future.get(SCALED_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        var resourceRequest = mShouldInterceptRequestHelper.getRequestsForUrl(resourceUrl);
+        Assert.assertNotNull(resourceRequest);
+
+        Assert.assertNull(
+                "The same-site=Strict cookie should not be attached to a cross-site request",
+                resourceRequest.getRequestHeaders().get("Cookie"));
+
+        var serverRequest = mWebServer.getLastRequest(resourcePath);
+        Assert.assertEquals(
+                "The server should not receive the same-site=Strict cookie",
+                "",
+                serverRequest.headerValue("Cookie"));
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"AndroidWebView", "Network"})
+    public void testInterceptedCookieHeaders_sameSiteStrictAllowed() throws Throwable {
+        // This test asserts that same-site=Strict cookies ARE provided on a same-site request
+        // to shouldInterceptRequest when setIncludeCookiesOnIntercept is enabled.
+        mActivityTestRule.getAwSettingsOnUiThread(mAwContents).setIncludeCookiesOnIntercept(true);
+
+        var cookieManager = mAwContents.getBrowserContextForPublicApi().getCookieManager();
+        cookieManager.removeAllCookies();
+
+        final String resourcePath = "/resource.txt";
+        final String resourceUrl = mWebServer.setResponse(resourcePath, "resource data", null);
+
+        // Set a SameSite=Strict cookie for the resource.
+        cookieManager.setCookie(resourceUrl, "session=SECRET; SameSite=Strict");
+
+        // Main page origin will be the same as resourceUrl because we don't use
+        // loadDataWithBaseUrl with a different origin.
+        mActivityTestRule.loadUrlSync(
+                mAwContents, mContentsClient.getOnPageFinishedHelper(), resourceUrl);
+
+        var resourceRequest = mShouldInterceptRequestHelper.getRequestsForUrl(resourceUrl);
+        Assert.assertNotNull(resourceRequest);
+
+        Assert.assertEquals(
+                "The same-site=Strict cookie SHOULD be attached to a same-site request",
+                "session=SECRET",
+                resourceRequest.getRequestHeaders().get("Cookie"));
+
+        // Also check that we didn't break anything in sending the cookies to the server.
+        var serverRequest = mWebServer.getLastRequest(resourcePath);
+        Assert.assertEquals(
+                "The server SHOULD receive the same-site=Strict cookie",
+                "session=SECRET",
+                serverRequest.headerValue("Cookie"));
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"AndroidWebView", "Network"})
+    public void testInterceptedCookieHeaders_sameSiteStrictAllowedNavigation() throws Throwable {
+        // This test asserts that same-site=Strict cookies ARE provided on a navigation request
+        // to shouldInterceptRequest when setIncludeCookiesOnIntercept is enabled.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldInterceptRequestTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldInterceptRequestTest.java
index 19b875f7..dbe5266 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldInterceptRequestTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldInterceptRequestTest.java
@@ -1521,6 +1521,133 @@
     @Test
     @SmallTest
     @Feature({"AndroidWebView", "Network"})
+    public void testInterceptedCookieHeaders_sameSiteStrictBlocked() throws Throwable {
+        // This test asserts that same-site=Strict cookies are not provided on a cross-site request
+        // to shouldInterceptRequest or the web server when setIncludeCookiesOnIntercept is enabled.
+        mActivityTestRule.getAwSettingsOnUiThread(mAwContents).setIncludeCookiesOnIntercept(true);
+
+        // setAcceptThirdPartyCookies is needed for the cross-site request to even consider cookies
+        // in WebView injection logic.
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> mAwContents.getSettings().setAcceptThirdPartyCookies(true));
+
+        var cookieManager = mAwContents.getBrowserContextForPublicApi().getCookieManager();
+        cookieManager.removeAllCookies();
+
+        final String resourcePath = "/resource.txt";
+        final List<Pair<String, String>> responseHeaders = new ArrayList<>();
+        // Allow CORS from some.origin.test so the fetch doesn't fail early.
+        responseHeaders.add(new Pair<>("Access-Control-Allow-Origin", "http://some.origin.test"));
+        final String resourceUrl =
+                mWebServer.setResponse(resourcePath, "resource data", responseHeaders);
+
+        // Set a SameSite=Strict cookie for the resource.
+        cookieManager.setCookie(resourceUrl, "session=SECRET; SameSite=Strict");
+
+        // Main page origin: http://some.origin.test (BASE_URL)
+        // fetch(resourceUrl, {credentials: 'omit'})
+        // credentials: 'omit' should definitely prevent cookies from being sent.
+        String fetchArgs = String.format("'%s', {credentials: 'omit'}", resourceUrl);
+        final Future<String> future = loadPageAndFetchInternal(null, fetchArgs);
+
+        Assert.assertEquals(
+                "fetch should succeed",
+                "cors",
+                future.get(SCALED_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        var resourceRequest = mShouldInterceptRequestHelper.getRequestsForUrl(resourceUrl);
+        Assert.assertNotNull(resourceRequest);
+
+        Assert.assertNull(
+                "The same-site=Strict cookie should not be attached to a cross-site request",
+                resourceRequest.getRequestHeaders().get("Cookie"));
+
+        var serverRequest = mWebServer.getLastRequest(resourcePath);
+        Assert.assertEquals(
+                "The server should not receive the same-site=Strict cookie",
+                "",
+                serverRequest.headerValue("Cookie"));
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"AndroidWebView", "Network"})
+    public void testInterceptedCookieHeaders_sameSiteStrictAllowed() throws Throwable {
+        // This test asserts that same-site=Strict cookies ARE provided on a same-site request
+        // to shouldInterceptRequest when setIncludeCookiesOnIntercept is enabled.
+        mActivityTestRule.getAwSettingsOnUiThread(mAwContents).setIncludeCookiesOnIntercept(true);
+
+        var cookieManager = mAwContents.getBrowserContextForPublicApi().getCookieManager();
+        cookieManager.removeAllCookies();
+
+        final String resourcePath = "/resource.txt";
+        final String resourceUrl = mWebServer.setResponse(resourcePath, "resource data", null);
+
+        // Set a SameSite=Strict cookie for the resource.
+        cookieManager.setCookie(resourceUrl, "session=SECRET; SameSite=Strict");
+
+        // Main page origin will be the same as resourceUrl because we don't use
+        // loadDataWithBaseUrl with a different origin.
+        mActivityTestRule.loadUrlSync(
+                mAwContents, mContentsClient.getOnPageFinishedHelper(), resourceUrl);
+
+        var resourceRequest = mShouldInterceptRequestHelper.getRequestsForUrl(resourceUrl);
+        Assert.assertNotNull(resourceRequest);
+
+        Assert.assertEquals(
+                "The same-site=Strict cookie SHOULD be attached to a same-site request",
+                "session=SECRET",
+                resourceRequest.getRequestHeaders().get("Cookie"));
+
+        // Also check that we didn't break anything in sending the cookies to the server.
+        var serverRequest = mWebServer.getLastRequest(resourcePath);
+        Assert.assertEquals(
+                "The server SHOULD receive the same-site=Strict cookie",
+                "session=SECRET",
+                serverRequest.headerValue("Cookie"));
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"AndroidWebView", "Network"})
+    public void testInterceptedCookieHeaders_sameSiteStrictAllowedNavigation() throws Throwable {
+        // This test asserts that same-site=Strict cookies ARE provided on a navigation request
+        // to shouldInterceptRequest when setIncludeCookiesOnIntercept is enabled.
+        mActivityTestRule.getAwSettingsOnUiThread(mAwContents).setIncludeCookiesOnIntercept(true);
+
+        var cookieManager = mAwContents.getBrowserContextForPublicApi().getCookieManager();
+        cookieManager.removeAllCookies();
+
+        final String pagePath = "/page.html";
+        final String pageUrl = mWebServer.setResponse(pagePath, "page data", null);
+
+        // Set a SameSite=Strict cookie for the page.
+        cookieManager.setCookie(pageUrl, "session=SECRET; SameSite=Strict");
+
+        // Navigate to the page. This is a top-level navigation, so SameSite=Strict cookies
+        // should be sent.
+        mActivityTestRule.loadUrlSync(
+                mAwContents, mContentsClient.getOnPageFinishedHelper(), pageUrl);
+
+        var resourceRequest = mShouldInterceptRequestHelper.getRequestsForUrl(pageUrl);
+        Assert.assertNotNull(resourceRequest);
+
+        Assert.assertEquals(
+                "The same-site=Strict cookie SHOULD be attached to a navigation request",
+                "session=SECRET",
+                resourceRequest.getRequestHeaders().get("Cookie"));
+
+        // Also check that we didn't break anything in sending the cookies to the server.
+        var serverRequest = mWebServer.getLastRequest(pagePath);
+        Assert.assertEquals(
+                "The server SHOULD receive the same-site=Strict cookie",
+                "session=SECRET",
+                serverRequest.headerValue("Cookie"));
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"AndroidWebView", "Network"})
     public void testInjectCorsFailure() throws Throwable {
         AwActivityTestRule.enableJavaScriptOnUiThread(mAwContents);
Loading diff…

Original Bug Report

reported by [email protected]

Potential SameSite=Strict/HttpOnly cookie leak in WebView via setIncludeCookiesOnIntercept

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.

Overview: If an Android WebView app enables setIncludeCookiesOnIntercept, the browser process intercepts requests and manually injects all cookies (including SameSite=Strict and HttpOnly) into the request headers. If the app does not override the request, this manually injected header bypasses the network stack’s standard SameSite enforcement and credentials: omit directives, allowing malicious cross-site pages to leak sensitive cookies.

Affected files:

  • android_webview/browser/network_service/aw_proxying_url_loader_factory.cc
  • android_webview/browser/aw_cookie_access_policy.cc
  • net/url_request/url_request_http_job.cc
  • services/network/public/cpp/header_util.cc
  • services/network/url_loader_util.cc

Estimated timestamp from git blame: 2025-09-30

Technical Analysis

A potential vulnerability exists in Android WebView where sensitive cookies can be leaked to cross-site network requests, bypassing SameSite=Strict and HttpOnly protections. This occurs when an application opts into WebSettingsCompat.setIncludeCookiesOnIntercept(true) and enables third-party cookies via setAcceptThirdPartyCookies(true).

  1. Over-permissive Cookie Retrieval: When a request is intercepted, AwProxyingURLLoaderFactory::InterceptedRequest::Restart() in android_webview/browser/network_service/aw_proxying_url_loader_factory.cc calls GetCookieHeader(). This function queries the CookieManager using net::CookieOptions::MakeAllInclusive(). This explicitly sets exclude_httponly to false and the SameSite context to SAME_SITE_STRICT, effectively retrieving every cookie associated with the request URL regardless of the actual request context (e.g., cross-site vs. same-site).

  2. Header Injection: The retrieved cookies are formatted into a standard HTTP Cookie line string and injected directly into request_.headers via InterceptWithCookieHeader(). This injection is not gated on request.credentials_mode or the actual SameSite relationship between the initiator and the target.

  3. Bypass of Network Stack Protections: If the application’s shouldInterceptRequest callback returns null (no override), the mutated request containing the manually injected Cookie header is forwarded to the inner network::URLLoaderFactory (typically CorsURLLoader).

    • Cookie is not currently in the kUnsafeHeaders list in services/network/public/cpp/header_util.cc, so CorsURLLoader does not strip it from the IPC payload.
    • In net/url_request/url_request_http_job.cc, if a request is made with credentials: 'omit' (or cross-origin with SameSite=Strict cookies), the network stack’s standard cookie logic determines no cookies should be added from the store. Because the stack has no new cookies to add, it never attempts to overwrite the existing, manually injected Cookie header.
    • The HttpNetworkTransaction then merges these extra headers and sends the request, successfully transmitting the protected cookies.

Potential Impact

An attacker-controlled page loaded in an affected WebView can perform cross-site requests (e.g., using fetch with mode: 'no-cors', credentials: 'omit' or via <img> tags) that will inadvertently include the victim site’s SameSite=Strict and HttpOnly cookies. This defeats CSRF protections and allows for credential leakage, bypassing browser isolation boundaries.

Preconditions

  1. The host application must call WebSettingsCompat.setIncludeCookiesOnIntercept(true).
  2. The host application must call CookieManager.setAcceptThirdPartyCookies(true).
  3. The application’s shouldInterceptRequest must return null for the targeted URL.

Suggested Reproduction Steps (Theoretical)

Note: Our tooling agent does not have the ability to run code, so these are potential reproduction steps based on code analysis.

  1. Build an Android app that embeds WebView and calls WebSettingsCompat.setIncludeCookiesOnIntercept(webSettings, true), CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true), and registers a WebViewClient whose shouldInterceptRequest() returns null.
  2. In that WebView, load https://victim.example/login and have the server set session=SECRET; HttpOnly; Secure; SameSite=Strict.
  3. Navigate the WebView to https://attacker.example/ which serves: <script>fetch('https://victim.example/account/transfer?to=attacker&amount=1000', {mode:'no-cors', credentials:'omit'});</script>.
  4. On victim.example, observe that the incoming request carries Cookie: session=SECRET even though the request is cross-site, credentials were omitted, and the cookie is SameSite=Strict.

Suggested Fix

If AwProxyingURLLoaderFactory decides to forward the request to the network stack without an application-provided override (ContinueAfterIntercept), it should explicitly strip the Cookie header it injected from request_.headers before calling target_factory_->CreateLoaderAndStart(). This ensures the network stack naturally evaluates and applies the correct cookies based on SameSite rules and credentials mode.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


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