CVE-2024-27081: Insufficient Information (NVD-CWE-noinfo)
value: ESPHome is a system to control your ESP8266/ESP32. A security misconfiguration in the edit configuration file API in the dashboard component of ESPHome version 2023.12.9 (command line installation) allows authenticated remote attackers to read and write arbitrary files under the configuration directory rendering remote code execution possible.  This vulnerability is patched in 2024.2.1.

CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-02-26T17:15Z
Repository: https://github.com/esphome/esphome
Language: Python
Files changed: esphome/dashboard/web_server.py
Commit date: 2024-02-26 07:52:08 +1300
Commit message: Merge pull request from GHSA-8p25-3q46-8q2p

Diff: diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py
index a53f5470..8bcc8efa 100644
--- a/esphome/dashboard/web_server.py
+++ b/esphome/dashboard/web_server.py
@@ -804,12 +804,20 @@ class InfoRequestHandler(BaseHandler):
 class EditRequestHandler(BaseHandler):
     @authenticated
     @bind_config
     async def get(self, configuration: str | None = None) -> None:
         """Get the content of a file."""
-        loop = asyncio.get_running_loop()
+        if not configuration.endswith((".yaml", ".yml")):
+            self.send_error(404)
+            return
+
         filename = settings.rel_path(configuration)
+        if Path(filename).resolve().parent != settings.absolute_config_dir:
+            self.send_error(404)
+            return
+
+        loop = asyncio.get_running_loop()
         content = await loop.run_in_executor(
             None, self._read_file, filename, configuration
         )
         if content is not None:
             self.write(content)
@@ -831,15 +839,21 @@ class EditRequestHandler(BaseHandler):
 
     @authenticated
     @bind_config
     async def post(self, configuration: str | None = None) -> None:
         """Write the content of a file."""
+        if not configuration.endswith((".yaml", ".yml")):
+            self.send_error(404)
+            return
+
+        filename = settings.rel_path(configuration)
+        if Path(filename).resolve().parent != settings.absolute_config_dir:
+            self.send_error(404)
+            return
+
         loop = asyncio.get_running_loop()
-        config_file = settings.rel_path(configuration)
-        await loop.run_in_executor(
-            None, self._write_file, config_file, self.request.body
-        )
+        await loop.run_in_executor(None, self._write_file, filename, self.request.body)
         # Ensure the StorageJSON is updated as well
         DASHBOARD.entries.async_schedule_storage_json_update(filename)
         self.set_status(200)
 
 

Vulnerable code:         loop = asyncio.get_running_loop()
        config_file = settings.rel_path(configuration)
        await loop.run_in_executor(
            None, self._write_file, config_file, self.request.body
        )
Fixed code:         if not configuration.endswith((".yaml", ".yml")):
            self.send_error(404)
            return

        if Path(filename).resolve().parent != settings.absolute_config_dir:
            self.send_error(404)
            return

        loop = asyncio.get_running_loop()
        if not configuration.endswith((".yaml", ".yml")):
            self.send_error(404)
            return

        filename = settings.rel_path(configuration)
        if Path(filename).resolve().parent != settings.absolute_config_dir:
            self.send_error(404)
            return

        await loop.run_in_executor(None, self._write_file, filename, self.request.body)