CVE-2026-11178
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifandroid_webview/browser/network_service/aw_proxying_url_loader_factory.cc |
modified |
Files Changed
android_webview/browser/network_service/aw_proxying_url_loader_factory.ccandroid_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldInterceptRequestTest.java
Patch
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.
Regression Test / PoC
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);
Original Bug Report
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.ccandroid_webview/browser/aw_cookie_access_policy.ccnet/url_request/url_request_http_job.ccservices/network/public/cpp/header_util.ccservices/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).
-
Over-permissive Cookie Retrieval: When a request is intercepted,
AwProxyingURLLoaderFactory::InterceptedRequest::Restart()inandroid_webview/browser/network_service/aw_proxying_url_loader_factory.cccallsGetCookieHeader(). This function queries theCookieManagerusingnet::CookieOptions::MakeAllInclusive(). This explicitly setsexclude_httponlyto false and the SameSite context toSAME_SITE_STRICT, effectively retrieving every cookie associated with the request URL regardless of the actual request context (e.g., cross-site vs. same-site). -
Header Injection: The retrieved cookies are formatted into a standard HTTP Cookie line string and injected directly into
request_.headersviaInterceptWithCookieHeader(). This injection is not gated onrequest.credentials_modeor the actual SameSite relationship between the initiator and the target. -
Bypass of Network Stack Protections: If the application’s
shouldInterceptRequestcallback returnsnull(no override), the mutated request containing the manually injectedCookieheader is forwarded to the innernetwork::URLLoaderFactory(typicallyCorsURLLoader).Cookieis not currently in thekUnsafeHeaderslist inservices/network/public/cpp/header_util.cc, soCorsURLLoaderdoes not strip it from the IPC payload.- In
net/url_request/url_request_http_job.cc, if a request is made withcredentials: 'omit'(or cross-origin withSameSite=Strictcookies), 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 injectedCookieheader. - The
HttpNetworkTransactionthen 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
- The host application must call
WebSettingsCompat.setIncludeCookiesOnIntercept(true). - The host application must call
CookieManager.setAcceptThirdPartyCookies(true). - The application’s
shouldInterceptRequestmust returnnullfor 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.
- Build an Android app that embeds WebView and calls
WebSettingsCompat.setIncludeCookiesOnIntercept(webSettings, true),CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true), and registers aWebViewClientwhoseshouldInterceptRequest()returnsnull. - In that WebView, load
https://victim.example/loginand have the server setsession=SECRET; HttpOnly; Secure; SameSite=Strict. - 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>. - On
victim.example, observe that the incoming request carriesCookie: session=SECRETeven though the request is cross-site, credentials were omitted, and the cookie isSameSite=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.