Search code examples
androidcookieswebviewandroid-webviewsamesite

Android WebView file:// urls with SameSite cookies


I'm having a cookie issue upgrading my app to Android API level 31 (Android 12). My file:// URLs are unable to access remote cookies, even when I set them to SameSite=None.

One of the major documented changes in API Level 31 is a change in cookie behavior.

Modern SameSite cookies in WebView

Android’s WebView component is based on Chromium, the open source project that powers Google’s Chrome browser. Chromium introduced changes to the handling of third-party cookies to provide more security and privacy and offer users more transparency and control. Starting in Android 12, these changes are also included in WebView when apps target Android 12 (API level 31) or higher.

The SameSite attribute of a cookie controls whether it can be sent with any requests, or only with same-site requests. The following privacy-protecting changes improve the default handling of third-party cookies and help protect against unintended cross-site sharing:

  • Cookies without a SameSite attribute are treated as SameSite=Lax.
  • Cookies with SameSite=None must also specify the Secure attribute, meaning they require a secure context and should be sent over HTTPS.
  • Links between HTTP and HTTPS versions of a site are now treated as cross-site requests, so cookies are not sent unless they are appropriately marked as SameSite=None; Secure.

My app includes embedded HTML files in the assets folder, which I display using a WebView via URLs like file:///android_asset/myfile.html.

Prior to API level 31, the WebView was able to communicate to my remote server, receive cookies, and send those cookies back in responses, but when I target API Level 31, the WebView refuses to retransmit the cookies that my server sends, even when I set SameSite=None.

Here's a trivial sample PHP file reproducing the issue.

<?php
header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']);
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');    // cache for 1 day

// Access-Control headers are received during OPTIONS requests
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {

    if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']))
        header("Access-Control-Allow-Methods: GET, POST, OPTIONS");

    if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']))
        header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");

    exit(0);
}

setcookie('x', "1", [
    'expires' => time() + 365*24*60*60,
    'path'=> '/',
    'secure' => true,
    'httponly' => true
]);

Header( "Content-Type: application/json");
echo "{\"hello\": \"".$_COOKIE['x']."\"}\n"
?>

And here's a sample HTML file, pointing to my domain www.choiceofgames.com for the time being (but you should use your own domain for testing):

<!DOCTYPE html>
<html>
<body></body>
<script type="module">
const url = 'https://www.choiceofgames.com/test/test.php';

let response;
response = await fetch(url, { credentials: "include" });
console.log(await response.text());

response = await fetch(url, { credentials: "include" });
console.log(await response.text());

</script>
</html>

When targeting API Level 30, the first console.log would return {"hello": ""}, and the second console.log would return {"hello": "1"}. Inspecting the WebView, I can see that the cookie gets sent in the second request.

When targeting API Level 31, it logs {"hello": ""} both times; the cookie isn't sent on the second request.

"OK," I thought. I'll just set SameSite=None on my cookie." I did it like this:

setcookie('x', "1", [
    'expires' => time() + 365*24*60*60,
    'path'=> '/',
    'secure' => true,
    'httponly' => true,
    'samesite' => None
]);

(I've made this version available as https://www.choiceofgames.com/test/test2.php for the time being.)

Adding SameSite=None made my Android WebView problem worse. It didn't fix my file:///android_asset/myfile.html WebView in API Level 31, but it did break my WebView in API Level 30; adding SameSite=None broke my old version, and fixed nothing in my new version.

As far as I can tell, SameSite=None just doesn't work at all in Android WebViews from file:// URLs.

That brings me to my questions:

  1. Can others repro the problem I'm having? Is it true that file:// URLs in Android WebViews just don't send cookies with SameSite=None, in both API Level 30 and in API Level 31? (Is this a fileable bug? Does anyone read or fix Android bugs filed by ordinary mortals like myself?)
  2. Is there a WebView WebSetting or something I can use to workaround this issue? (I'm currently using setAllowUniversalAccessFromFileURLs(true).
  3. Can you suggest another way I can workaround this issue?

Solution

  • https://chromium.googlesource.com/chromium/src/+/1d127c933a4a39c65dc32cbd35bd511fd68ea452/android_webview/browser/cookie_manager.cc#317

    // There are some unknowns about how to correctly handle file:// cookies,
    // and our implementation for this is not robust.  http://crbug.com/582985
    

    It looks like the "best" way to load asset files in Android is not to use file:///android_asset/myfile.html, but to use a WebViewAssetLoader.

    WebViewAssetLoader intercepts WebView requests, making all of your asset files appear on a fake HTTPS domain URL https://appassets.androidplatform.net/assets. Instead of file:///android_asset/myfile.html you'd load from https://appassets.androidplatform.net/assets/myfile.html.

    The browser will treat that like a "real" HTTPS domain. SameSite=None will work normally, CORS will have a conventional non-null Origin, and there won't be any weirdness around sharing cookies between file:// URLs.

    (But, even better than SameSite=None would be to use a fake subdomain of your actual domain. WebViewAssetLoader has a builder parameter allowing you to set the domain to a domain you control, e.g. if you own example.com, you could host assets on https://appassets.example.com, allowing you to share cookies with your website even with SameSite=Strict.)