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 19b875f723c7b..ef0489ad3be5d 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 @@ -1467,6 +1467,82 @@ public class AwContentsClientShouldInterceptRequestTest extends AwParameterizedT } } + /** + * POC: when setIncludeCookiesOnIntercept(true) is enabled and the app's + * shouldInterceptRequest() returns null, the Cookie header that + * AwProxyingURLLoaderFactory injected into the ResourceRequest (using + * CookieOptions::MakeAllInclusive(), i.e. ignoring SameSite + HttpOnly) + * is forwarded unchanged to the real network stack and reaches the wire. + * + * This test proves it via the credentials:'omit' variant: a fetch() with + * credentials:'omit' must, per the Fetch spec, send **no** cookies at all. + * URLRequestHttpJob honours that by skipping its cookie path entirely — + * which means it never *clears* the proxy-injected header either, so the + * SameSite=Strict / HttpOnly cookie is sent over the network even though + * the page explicitly asked for no credentials. + * + * The same mechanism leaks the cookie on a genuine cross-site request (the + * SameSite=Strict bypass): whenever the net stack's own SameSite-filtered + * cookie list comes up empty, SetCookieHeaderAndStart() never overwrites + * the injected header. + * + * On a fixed build the final assertion FAILS (no Cookie header on wire). + * On a vulnerable build it PASSES, demonstrating the leak. + */ + @Test + @SmallTest + @Feature({"AndroidWebView", "Network"}) + public void testIncludeCookiesOnIntercept_leaksToNetworkOnCredentialsOmit() + throws Throwable { + AwActivityTestRule.enableJavaScriptOnUiThread(mAwContents); + + // App opt-ins that enable the vulnerable path. + mActivityTestRule + .getAwSettingsOnUiThread(mAwContents) + .setIncludeCookiesOnIntercept(true); + mActivityTestRule + .getAwSettingsOnUiThread(mAwContents) + .setAcceptThirdPartyCookies(true); + + var cookieManager = mAwContents.getBrowserContextForPublicApi().getCookieManager(); + cookieManager.setAcceptCookie(true); + cookieManager.removeAllCookies(); + + // Victim endpoint on the real network server. + final String victimPath = "/victim_api"; + final String victimUrl = mWebServer.setResponse(victimPath, "ok", new ArrayList<>()); + + // Plant a SameSite=Strict; HttpOnly cookie for the victim origin. + cookieManager.setCookie( + victimUrl, "session=SECRET-STRICT-HTTPONLY; HttpOnly; SameSite=Strict"); + Assert.assertNotNull( + "precondition: cookie stored", cookieManager.getCookie(victimUrl)); + + // Attacker page issues a fetch() with credentials:'omit'. Per spec this + // must carry NO cookies. We do NOT enqueue an intercept response, so + // shouldInterceptRequest returns null and the request falls through to + // the real network URLLoaderFactory. + final String attackerHtml = + ""; + final String attackerUrl = + addPageToTestServer(mWebServer, "/attacker.html", attackerHtml); + + mActivityTestRule.loadUrlSync( + mAwContents, mContentsClient.getOnPageFinishedHelper(), attackerUrl); + mActivityTestRule.pollUiThread(() -> mWebServer.getRequestCount(victimPath) >= 1); + + String cookieOnWire = mWebServer.getLastRequest(victimPath).headerValue("Cookie"); + // VULNERABLE BUILD: passes — SameSite=Strict;HttpOnly cookie reached the + // network despite credentials:'omit'. FIXED BUILD: cookieOnWire is "". + Assert.assertEquals( + "BUG REPRO: SameSite=Strict;HttpOnly cookie leaked onto a " + + "credentials:'omit' network request", + "session=SECRET-STRICT-HTTPONLY", + cookieOnWire); + } + private static Set getCookies(AwCookieManager cookieManager, String destinationUrl) { String cookie = cookieManager.getCookie(destinationUrl); Assert.assertNotNull(cookie);