apache / cordova-android

Apache Cordova Android
https://cordova.apache.org/
Apache License 2.0
3.66k stars 1.54k forks source link

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

Closed lardyNiji closed 3 years ago

lardyNiji commented 3 years ago

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 :

Result in Chrome console :

The XMLHttpRequest in status 200 with Google.com site content

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

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

breautek commented 3 years ago

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 commented 3 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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.