ankraft / ACME-oneM2M-CSE

An open source CSE Middleware for Education.
https://acmecse.net/
BSD 3-Clause "New" or "Revised" License
23 stars 16 forks source link

oneM2M Cross Origin Requests #93

Closed tsengia closed 1 year ago

tsengia commented 1 year ago

Hi Andreas,

I have a feeling that Bob Flynn has already mentioned this to you, but I am current developing a React JS single page application (SPA) that requests data from the ACME CSE.

The problem with this approach that I am taking is that the SPA has to be hosted on a different IP (and/or port number) than the ACME CSE. This means that when the JavaScript code in the SPA makes an XHR/AJAX/HTTP request to the ACME CSE, the user's web browser will block the response if the ACME CSE does not specifically allow the Cross Origin Request.

For now, I've created a very bad workout one my own fork and branch: https://github.com/ExpandingDev/ACME-oneM2M-CSE/tree/cors-shim The changes I make in there do two things:

  1. Adds flask-cors as a dependency
  2. Uses the flask cors to enable CORS requests from ALL origins. This is a security vulnerability.

I am aware that the way I am getting around these CORS problems is a hack and should not be used in the mainstream ACME CSE/oneM2M.

To me, the ideal solution would be something like this:

  1. Allow oneM2M to treat the Origin header as the same as the X-M2M-Origin header
  2. Ignore the X-M2M-Origin header if the plain Origin header is present
  3. Reply with Access-Control-Allow-Origin only if the Origin in the response matches with an originator specified in an ACP

I don't fully know if this would be a secure approach, I would be then concerned about attackers registering a domain name to match the originator present in an ACP in order to get their payload through.

ankraft commented 1 year ago

Hi Tyler,

yes, Bob and I shortly discussed this problem earlier today. I am hesitating a bit to add CORS support into ACME's web server because I have the feeling that this is the wrong approach, but I am still trying to get my head around it.

To your points. I don't understand why you suggest that Origin and X-M2M-Origin are related. The first one deals with the http / CORS aspects of the http protocol, and X-M2M-Origin is a oneM2M identifier that identifies the originator of a oneM2M request.

Allow oneM2M to treat the Origin header as the same as the X-M2M-Origin header

The X-M2M-Origin is a URI but not necessary a URL. Its content must follow the format defined for AE-ID or CSE-ID, while Origin as I understand it must be the URL of the original website where the browser loaded the web page and the JS content, right? So my understanding is that both values are actually two different things.

Ignore the X-M2M-Origin header if the plain Origin header is present

Why? The browser application (ie. the AE) needs to identify itself. This is done through the X-M2M-Origin header. And a request must have the oneM2M originator, otherwise it is invalid and will be rejected.

Reply with Access-Control-Allow-Origin only if the Origin in the response matches with an originator specified in an ACP

That could be an interesting addition. There is actually a a field in ACP resources that, if present, enables the use of one particular ACP resource for an IP address. Unfortunately, I haven't implemented this yet.

Another quick solution could be to add a list of sites to the configuration that are allowed to do CORS, and ignore ACPs for the moment.


Why I am still hesitating is that in normal deployments this is a functionality that is not realised by the actual backend server (or here: the CSE), but by a reverse proxy that is part of an API gateway and routes the request accordingly. So, both the requests to the web site's http server as well as the oneM2M Mca requests would go through this reverse proxy first.

Actually, I did this for the stand-alone version of ACME's web UI. When run as a stand-alone the web UI's server forward oneM2M requests to the actual CSE (which would run on a different address/port). I faced the same problems here as you do. See https://github.com/ankraft/ACME-oneM2M-CSE/blob/master/acme/webui/webUI.py#L92 for the simple implementation. You can also use nginx, of course.

This could be a quick fix that unfortunately adds another component to your infrastructure but wouldn't require any further change.

Another option would be to not send the oneM2M requests over http but via MQTT. This would require an MQTT broker of course, but would eliminate the CORS problem The oneM2M requests over look similar to the http requests, only the packaging is a bit different (actually, those would be "pure" oneM2M requests). You can try this yourself by installing a broker (like https://mosquitto.org), enabling and configuring MQTT for ACME to connect to the broker, and then configure the test suite to use MQTT. By reading the logs you should get a good idea about the request format. Unfortunately, this would add more efforts for your installation as well as to add MQTT to your JS application.

tsengia commented 1 year ago

Origin as I understand it must be the URL of the original website where the browser loaded the web page and the JS content

I agree with what you have said about the Origin header. However, the true value of using the Origin header is because it is a "protected" header field that is automatically set by the web browser. This protected status means that JavaScript code is not allowed to modify it (ie. malicious JavaScript code on a webpage cannot alter it). The ACME CSE can consider it to be a truthful indicator of where the request is coming from.

The same is not true of the X-M2M-Origin header. For web browsers, it is not a protected header field, so malicious JavaScript code can spoof that header to be anything it wants, which is why I suggest ignoring the X-M2M-Origin header if the Origin header exists.

The X-M2M-Origin is a URI but not necessary a URL. Its content must follow the format defined for AE-ID or CSE-ID

The idea behind using the Origin header instead of X-M2M-Origin is that now webpage becomes the AE. If you were to create an AE with the same ID as the website's URL, the JavaScript running on the webpage would now have access to the CSE (and any other resources that are allowed via the ACPs).

The reason that this change could be advantageous is that this would allow pure JavaScript web apps can now connect to the oneM2M CSE, without the need to have a backend hosted on the internet. Without this, you would have to use the reverse proxy as you mentioned. But if you're already using a reverse proxy, why not just use your own backend database instead of using oneM2M?


To be clear, I am not suggesting that you add these changes to ACME. I am just thinking of ideas for improving oneM2M. Something like this would definitely have to get approved by the standard before adding it in.

ankraft commented 1 year ago

There are a lot of interesting thoughts that we need to thing about and discuss. Bob and I will bring them definitely to the discussions within oneM2M. Thanks a lot for the discussion!

There is one thing that is important to consider. The http binding is only one mapping to a concrete transport technology. Other are MQTT, CoAP or WebSockets. Each of these bindings is a mapping of the abstract oneM2M requests, and so is the oneM2M origin request attribute (that holds the ID of the sender, the "originator") mapped to the X-M2M-Origin http header field.

The problem is now that the format of this origin attribute must follow a certain format. For example, it must usually start with an upper-case "C", like "CAdmin". It might have in addition the CSE-ID to identify the CSE node where this originator is registered, like "/id-in/CAdmin". But that's about it. It cannot be an http URL, like "https://example.com".

Another issue is that this URL usually is a constant value (at least that is my understanding for the http(!) origin header field). This means that any instance of the web page would have the same originator, and so this value cannot be used to identify a particular instance of a web application. Each instance of an AE has a unique oneM2M originator.

Now, is it possible to enable the browser to assign a unique oneM2M-valid ID to the http origin header?

What I don't understand yet is why you want to replace the X-M2M-Origin header in the request? There are other X-M2M-* header that you must set for a valid request anyway.

But if you're already using a reverse proxy, why not just use your own backend database instead of using oneM2M?

That is a good question. In an enterprise deployment one would never expose backend services (or a CSE) directly to the public Internet, to a browser or to any other application directly. There is always an API gateway and/or a UI API layer in between (for authentication, load balancing, redirections etc).

Another important point is that a oneM2M CSE is not just a database. Well, it offers data handling functionality in the form of storing data in the resource tree, but there is so much more IoT-related functionality that a normal database doesn't implement.


I had a quick look at "flask-cors". You wrote that you integrated it in your ACME clone. It is really that simple to integrate as examples on the project's GH page suggest? (https://github.com/corydolphin/flask-cors) Are alle the headers added automatically for responses? In that case this could be a nice addition to ACME (configurable, of course, and switched off by default).

ankraft commented 1 year ago

I am now adding CORS support directly in ACME. A first implementation with flask-cors was straightforward to do. Next will be a test by running a test in a browser as well as adding some configuration options here.

ankraft commented 1 year ago

My tests with a browser (Safari & Firefox) went well. I tidied up the code, added configurations and documentations, and committed the changes to the development branch. There is no a [server.http.cors] section where CORS can be enabled.

Please let me know whether this works for you.

One note: I found that browsers refused to do the CORS procedures if the CSE isn't running with TLS (https) enabled. You still might need to accept a self-signed certificate, though, however the browsers don't ask when they trying to do the CORS test. I managed this by accessing ACME's web UI from the browser and then accepting an exception for that certificate. Then the certificate is also accepted for CORS.