Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XHR Request fail with CORS Access-Control-Allow-Origin on Cordova android 10 #1354

Closed
3 tasks done
lardyNiji opened this issue Sep 17, 2021 · 12 comments
Closed
3 tasks done

Comments

@lardyNiji
Copy link

Bug Report

Problem

Simple GET xhr request (https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests) in cordova-android@^10.0.0 trigger CORS

What is expected to happen?

Simple xhr GET request should not trigger CORS

What does actually happen?

Simple xhr GET request should trigger CORS

Example:
Access to XMLHttpRequest at 'https://www.google.com/' from origin 'https://localhost' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Information

I have tested with two Cordova applications out of the box :

  • The first one in Cordova 9.1 works fine the GET xhr call retrieve the remote site content
    XHR request save as CURL from Chrome network

curl 'https://www.google.com/'
-H 'authority: www.google.com'
-H 'user-agent: Mozilla/5.0 (Linux; Android 11; SM-G991B Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/93.0.4577.82 Mobile Safari/537.36'
-H 'accept: /'
-H 'x-requested-with: com.example.hellocdv9'
-H 'sec-fetch-site: cross-site'
-H 'sec-fetch-mode: cors'
-H 'sec-fetch-dest: empty'
-H 'accept-language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7'
-H 'cookie: CONSENT=PENDING+960'
--compressed

Result in Chrome console :

The XMLHttpRequest in status 200 with Google.com site content

  • The second one in Cordova 10.1.1 does not work and is blocked by CORS
    XHR request save as CURL from Chrome network

curl 'https://www.google.com/'
-H 'authority: www.google.com'
-H 'user-agent: Mozilla/5.0 (Linux; Android 11; SM-G991B Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/93.0.4577.82 Mobile Safari/537.36'
-H 'accept: /'
-H 'origin: https://localhost'
-H 'x-requested-with: com.example.hellocdv10'
-H 'sec-fetch-site: cross-site'
-H 'sec-fetch-mode: cors'
-H 'sec-fetch-dest: empty'
-H 'referer: https://localhost/'
-H 'accept-language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7'
--compressed

Result in Chrome console :

The XMLHttpRequest in status 0 with the following error
Access to XMLHttpRequest at 'https://www.google.com/' from origin 'https://localhost' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Command or Code

I've created two Cordova applications

  • Cordova 9.x

cordova create hellocdv9 com.example.hellocdv9 HelloWorldcdv9
cd hellocdv9/
cordova platform add android
cordova build android
adb install ./platforms/android/app/build/outputs/apk/debug/app-debug.apk

  • Cordova 10.x

cordova create hellocdv10 com.example.hellocdv10 HelloWorldcdv10
cd hellocdv10/
cordova platform add android@^10.0.0
cordova plugin remove cordova-plugin-whitelist
cordova build android
adb install ./platforms/android/app/build/outputs/apk/debug/app-debug.apk

  • File changes for both applications

www/index.html

32c32
<         <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; img-src 'self' data: content:;">
---
>         <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; img-src 'self' data: content:;">

www/js/index.js

28a29,37
>     let xhr = new XMLHttpRequest();
>     xhr.open('GET', 'https://www.google.com');
>     xhr.onload = function() {
>         console.log(xhr, xhr.responseText);
>     };
>     xhr.onerror = function(e) {
>         console.error(e, xhr);
>     };
>     xhr.send();

Environment, Platform, Device

Device

android: 11
target api: 30

Version information

First app

cordova: 9.1.0
cordova-plugin-whitelist: 1.3.5

Second App

cordova: 10.1.1
No plugin

Cordova Cli: 10.0.0

Checklist

  • I searched for existing GitHub issues
  • I updated all Cordova tooling to most recent version
  • I included all the necessary information above
@breautek
Copy link
Contributor

breautek commented Sep 17, 2021

Some background knowledge...

Content-Security-Policy is a different security mechanism than CORS (Cross-Origin Resource Sharing).

In cordova-android@10, we implemented something called a WebAssetLoader, which proxies requests through https://localhost protocol. The WebAssetLoader kind of acts like a private web server only accessible to your app. This was done because some web view features requires you to be on a "secure context" (e.g https) for the features to be enabled. In doing so, it does enable the CORS enforcement.

Cordova android 9.x uses the plain old file system (file://) which didn't enforced CORs. This is why you see the XHR request work in 9.x, but not in 10.x. You can make 10.x behave like 9.x by enabling the AndroidInsecureFileModeEnabled preference:

<preference name="AndroidInsecureFileModeEnabled" value="true" />

But let's assume you don't want to use this workaround

CORS is a security mechanism for CORS-enabled browsers that are controlled by the backend server. So in this case, https://google.com must provide the required response headers for the browser to accept the request response. They do not provide the Access-Control-Allow-Origin: https://localhost or Access-Control-Allow-Origin: * response header, therefore the request is rejected by the browser / webview.

There is no API available in the webview to disable CORS. Assuming you don't have access to https://google.com to make the appropriate backend change, the only workaround at this point is to not use the browser's request mechanism (neither fetch() or XMLHttpRequest) and instead find/build a cordova plugin that does a native request, which is not bounded by CORS.

Another approach is to configure a proxy server that is configured to use the CORS protocol in which your app can make request to, which will be redirected to https://google.com, then you can relay the response back to the client. This approach will still allow you to use the browser's HTTP request APIs.

Now that we got all that information out there... May I ask more details on your use case?

@lardyNiji
Copy link
Author

First of all thank you for the quick and detailled answer.

We are trying to update cordova 9 existing apps which embend some xhr calls to an api and some other resources.

These apps are internal apps not delivered on Google Play store then i think that we are going to use the workaround with cordova preferences you have explained in your first point.

I've tested it with my hellocdv10 demo app and this configuration do the job.

@Ahmed-Abdelftah
Copy link

Ahmed-Abdelftah commented Jun 9, 2022

Some background knowledge...

Content-Security-Policy is a different security mechanism than CORS (Cross-Origin Resource Sharing).

In cordova-android@10, we implemented something called a WebAssetLoader, which proxies requests through https://localhost protocol. The WebAssetLoader kind of acts like a private web server only accessible to your app. This was done because some web view features requires you to be on a "secure context" (e.g https) for the features to be enabled. In doing so, it does enable the CORS enforcement.

Cordova android 9.x uses the plain old file system (file://) which didn't enforced CORs. This is why you see the XHR request work in 9.x, but not in 10.x. You can make 10.x behave like 9.x by enabling the AndroidInsecureFileModeEnabled preference:

<preference name="AndroidInsecureFileModeEnabled" value="true" />

But let's assume you don't want to use this workaround

CORS is a security mechanism for CORS-enabled browsers that are controlled by the backend server. So in this case, https://google.com must provide the required response headers for the browser to accept the request response. They do not provide the Access-Control-Allow-Origin: https://localhost or Access-Control-Allow-Origin: * response header, therefore the request is rejected by the browser / webview.

There is no API available in the webview to disable CORS. Assuming you don't have access to https://google.com to make the appropriate backend change, the only workaround at this point is to not use the browser's request mechanism (neither fetch() or XMLHttpRequest) and instead find/build a cordova plugin that does a native request, which is not bounded by CORS.

Another approach is to configure a proxy server that is configured to use the CORS protocol in which your app can make request to, which will be redirected to https://google.com, then you can relay the response back to the client. This approach will still allow you to use the browser's HTTP request APIs.

Now that we got all that information out there... May I ask more details on your use case?

This maybe the most detailed answer I have ever read , thank you!

@massimilianocom
Copy link

massimilianocom commented Jul 12, 2022

hello after so much banging on this problem finally one that makes me understand everything.
This:

How do I understand it makes you emulate the device like it's a version 9?

Go to the config.xml file and at what point?

Why did I do this:

<platform name = "android">
         <preference name = "AndroidInsecureFileModeEnabled" value = "true" />
         <icon src = "res / android / icon.png" />
         <allow-intent href = "market: *" />
         <resource-file src = "resources / android / xml / network_security_config.xml" target = "app / src / main / res / xml / network_security_config.xml" />
         <access origin = "*" />
     </platform>

But it still doesn't work in me the state has always been 0

@TDola
Copy link

TDola commented Nov 8, 2022

Some background knowledge...

Content-Security-Policy is a different security mechanism than CORS (Cross-Origin Resource Sharing).

In cordova-android@10, we implemented something called a WebAssetLoader, which proxies requests through https://localhost protocol. The WebAssetLoader kind of acts like a private web server only accessible to your app. This was done because some web view features requires you to be on a "secure context" (e.g https) for the features to be enabled. In doing so, it does enable the CORS enforcement.

Cordova android 9.x uses the plain old file system (file://) which didn't enforced CORs. This is why you see the XHR request work in 9.x, but not in 10.x. You can make 10.x behave like 9.x by enabling the AndroidInsecureFileModeEnabled preference:

<preference name="AndroidInsecureFileModeEnabled" value="true" />

But let's assume you don't want to use this workaround

CORS is a security mechanism for CORS-enabled browsers that are controlled by the backend server. So in this case, https://google.com must provide the required response headers for the browser to accept the request response. They do not provide the Access-Control-Allow-Origin: https://localhost or Access-Control-Allow-Origin: * response header, therefore the request is rejected by the browser / webview.

There is no API available in the webview to disable CORS. Assuming you don't have access to https://google.com to make the appropriate backend change, the only workaround at this point is to not use the browser's request mechanism (neither fetch() or XMLHttpRequest) and instead find/build a cordova plugin that does a native request, which is not bounded by CORS.

Another approach is to configure a proxy server that is configured to use the CORS protocol in which your app can make request to, which will be redirected to https://google.com, then you can relay the response back to the client. This approach will still allow you to use the browser's HTTP request APIs.

Now that we got all that information out there... May I ask more details on your use case?

Is there a way to change what it sends as the source? We normally use appname.companyname.com in our URLs.

@breautek
Copy link
Contributor

breautek commented Nov 8, 2022

How do I understand it makes you emulate the device like it's a version 9?

CORS is a browser feature, so the feature isn't tied to Android versions. It depends on the Android Webview version that happens to be running. However I'm not sure when exactly CORS started being enforced in the Android Webview.

iOS is slightly different in that their system webview is tied to the OS, and in particular they started enforcing CORS with their WKWebView available in iOS 9. Android Webview is an upgradeable package, independent from the OS so you can still have an Android 9 device (or an app running cordova-android@9) and still encounter CORS issues.

Is there a way to change what it sends as the source? We normally use appname.companyname.com in our URLs.

If by source, you mean the origin value, you have limited control over it. The origin is the scheme and domain of the document, or null if there is no domain (such as when using the filesystem file:// protocol).

Starting on cordova-android@10 we have support for scheme handlers allowing you to change the scheme of the app, instead of using the file:// approach as we've had in the past. The purpose of this is actually heavily influenced by CORS as all filesystem based activity are considered cross-origin by default, whereas doing an XHR request against your own scheme will have relaxed CORS rules since it will be considered part of the same region.

For android, the default scheme if enabled is https://localhost if I recall correctly, but you can change it using the following preferences:

<preference name="scheme" value="https" /> <!-- This requires cordova-android@10.1 or later -->
<preference name="hostname" value="localhost" />

Note that for Android, the scheme must be either https or http, but the hostname can be any valid domain-like value. Do note that the scheme system operates kind of like an interception, so if you choose the same scheme as a real server, that server will not be reachable as requests will simply get directed to the scheme system instead of the actual network. So you should choose something that is unique for your app, that won't conflict with real servers. Learn More

So by changing the scheme settings, you can influence the origin value. Don't forget that web storage features like local storage are tied to origins, each origin have their own database so by changing the scheme settings/origin, you will lose access to previously stored web storage data.

@TDola
Copy link

TDola commented Nov 8, 2022

How do I understand it makes you emulate the device like it's a version 9?

CORS is a browser feature, so the feature isn't tied to Android versions. It depends on the Android Webview version that happens to be running. However I'm not sure when exactly CORS started being enforced in the Android Webview.

iOS is slightly different in that their system webview is tied to the OS, and in particular they started enforcing CORS with their WKWebView available in iOS 9. Android Webview is an upgradeable package, independent from the OS so you can still have an Android 9 device (or an app running cordova-android@9) and still encounter CORS issues.

Is there a way to change what it sends as the source? We normally use appname.companyname.com in our URLs.

If by source, you mean the origin value, you have limited control over it. The origin is the scheme and domain of the document, or null if there is no domain (such as when using the filesystem file:// protocol).

Starting on cordova-android@10 we have support for scheme handlers allowing you to change the scheme of the app, instead of using the file:// approach as we've had in the past. The purpose of this is actually heavily influenced by CORS as all filesystem based activity are considered cross-origin by default, whereas doing an XHR request against your own scheme will have relaxed CORS rules since it will be considered part of the same region.

For android, the default scheme if enabled is https://localhost if I recall correctly, but you can change it using the following preferences:

<preference name="scheme" value="https" /> <!-- This requires cordova-android@10.1 or later -->
<preference name="hostname" value="localhost" />

Note that for Android, the scheme must be either https or http, but the hostname can be any valid domain-like value. Do note that the scheme system operates kind of like an interception, so if you choose the same scheme as a real server, that server will not be reachable as requests will simply get directed to the scheme system instead of the actual network. So you should choose something that is unique for your app, that won't conflict with real servers. Learn More

So by changing the scheme settings, you can influence the origin value. Don't forget that web storage features like local storage are tied to origins, each origin have their own database so by changing the scheme settings/origin, you will lose access to previously stored web storage data.

Awesome, thank you very much

@sarathi0333
Copy link

sarathi0333 commented Nov 14, 2022

I am commenting here due to Android 12 Behaviour changes.
@breautek In the above reply, you said "if you choose the same scheme as a real server, that server will not be reachable as requests will simply get directed" If suppose my remote server is https://abc.com should I not use "scheme" value as 'https'? and the value should be some like below?
<preference name="scheme" value="httpsapps" />

Can you give me an example? I am getting the below error when I give scheme as https and hostname as abc.com(remote server domain name) "Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Orgin'

@breautek
Copy link
Contributor

On Android, scheme must be either http or https. Android doesn't accept any other value for the scheme.

I'm not sure what happens if you're real server is https://abc.com and you set to scheme / hostname to http://abc.com, but I think it will work, as long as you don't try to hit http://abc.com and expect to make a request to your real server. In most cases leaving the defaults should be sufficient though. Using https://localhost is guaranteed not to conflict with any real servers. It is up to the server however to read the Origin request header and set the Access-Control response headers appropriately.

@sarathi0333
Copy link

sarathi0333 commented Nov 14, 2022

Initially, I had the default config https://localhost now I updated the android target SDK to 31. After this, I started getting issues. My REST API returns a cookie and tries to set it. But it throws the error
"This set-cookie header didn't specify a samesite attribute and was defaulted to samesite=lax and was blocked because it came from a cross-site response which was not the response to top-level navigation. The set-cookie had to have been set with "SameSite=None" to enable cross-site usage.

Note: I cannot change the cookie response on the server side. It's an ionic mobile app using Cordova.

To overcome the above issue, I tried to configure a custom scheme and hostname equal to my real server. After which I started getting "Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin".

@TDola
Copy link

TDola commented Nov 14, 2022

Initially, I had the default config https://localhost now I updated the android target SDK to 31. After this, I started getting issues. My REST API returns a cookie and tries to set it. But it throws the error "This set-cookie header didn't specify a samesite attribute and was defaulted to samesite=lax and was blocked because it came from a cross-site response which was not the response to top-level navigation. The set-cookie had to have been set with "SameSite=None" to enable cross-site usage.

Note: I cannot change the cookie response on the server side. It's an ionic mobile app using Cordova.

To overcome the above issue, I tried to configure a custom scheme and hostname equal to my real server. After which I started getting "Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin".

Wish I could help but I started having this problem a year or so ago and had to switch to the plugin cordova-plugin-advanced-http to bypass cookie security while we worked to remove all cookie authentication. All our problems are gone now that we switched to token auth. Cookies are a dead tech for use as logins. You can do the same, use that plugin to keep things going while you replace your cookies. https://learn.g2.com/cookieless-future

@breautek
Copy link
Contributor

breautek commented Nov 15, 2022

Note: I cannot change the cookie response on the server side. It's an ionic mobile app using Cordova.

Cookie policies, like the Access-Control CORS headers, are set by the server. If yo do not have access to the server side to allow cross origin cookies, then you effectively don't have permission to communicate with that server and that webserver only supports standard browsers that connect to the webserver directly.

To overcome the above issue, I tried to configure a custom scheme and hostname equal to my real server. After which I started getting "Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin".

This is wrong, because by setting your scheme scheme/hostname you won't actually hit your real server. The request gets intercepted by something called WebViewAssetLoader to load local files. It sounds like you're using a POST or some other HTTP method other than GET which triggers preflight requests which is why you see a CORS issue here.

Cookies are intended to be set to it's own origin. It's a potential security risk to expose cookies cross origin, but with server configuration access, you can set the samesite to None, allowing cookies to be sent cross-origin. Imo, cookies is a dated technology that worked well in the past when your only client was a web browser hitting a webserver directly (that is the web browser is being served content directly from the server). While cordova runs in a webview, you generally don't load the document itself from a remote server. You need to have the app support offline where the server may not be reachable. So you bundle web assets with the mobile app itself, which makes the app cross-origin. EThis is what makes cookie-based authentication a poor choice for an authentication mechanism. It works ok if your only client is a standard web browser, but if you intend to support multiple different kinds of clients, cookie-based authentication tends to falls apart, especially now that browser vendors are locking down cookies to improve on security and privacy issues.

This thread is getting off topic so in order to respect the OP's inbox, I'm going to lock it here. If you have further questions on this matter, I'd suggest asking our Slack community. If you believe you've found a bug, then feel free to create a new issue.

Edit: For those who have access to the server to edit cookie policies, you might be able to explicitly set your Domain value to of the cookie to the base domain. For example, Set-Cookie <cookie>; Domain=example.com

Then have your app scheme be set to a subdomain of that domain, e.g. https://myapp.example.com.

MDN states

Multiple host/domain values are not allowed, but if a domain is specified, then subdomains are always included.

And they also state:

A cookie is associated with a particular domain and scheme (such as http or https), and may also be associated with subdomains if the Set-Cookie Domain attribute is set. If the cookie domain and scheme match the current page, the cookie is considered to be from the same site as the page, and is referred to as a first-party cookie.

So this leads me to believe that it is possible to configure the server and the app in a way that the app will treat cookies as first-party cookies, but this needs to be tested and this still requires server side configurations.

@apache apache locked as resolved and limited conversation to collaborators Nov 15, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants