mirror of
https://github.com/esphome/esphome.git
synced 2026-09-22 12:38:40 +00:00
Merge remote-tracking branch 'origin/dev' into integration
This commit is contained in:
+1
-1
@@ -445,7 +445,7 @@ esphome/components/select/* @esphome/core
|
||||
esphome/components/sen0321/* @notjj
|
||||
esphome/components/sen21231/* @shreyaskarnik
|
||||
esphome/components/sen5x/* @martgras
|
||||
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
|
||||
esphome/components/sen6x/* @martgras @mebner86 @tuct
|
||||
esphome/components/sendspin/* @kahrendt
|
||||
esphome/components/sendspin/media_player/* @kahrendt
|
||||
esphome/components/sendspin/media_source/* @kahrendt
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
# ESPHome Threat Model
|
||||
|
||||
This document defines the trust boundary for the **ESPHome** repository — the
|
||||
Python compiler/CLI and the device firmware it generates — so that real security
|
||||
bugs can be told apart from defense-in-depth improvements. It gives contributors,
|
||||
reviewers, and security researchers a clear answer to one question:
|
||||
**does this issue let an _unauthenticated_ attacker do something they shouldn't?**
|
||||
|
||||
Related documents:
|
||||
|
||||
- Deployment guidance for operators:
|
||||
https://esphome.io/guides/security_best_practices/
|
||||
- The **Device Builder dashboard** (the web UI, its authentication, ingress,
|
||||
Origin/Host gates, and peer-link pairing) lives in a separate repository and
|
||||
has its own threat model. If your report concerns any of that, please read and
|
||||
report there instead:
|
||||
https://github.com/esphome/device-builder/blob/main/docs/THREAT_MODEL.md
|
||||
|
||||
## The trust boundary
|
||||
|
||||
For this repository there are two trusted inputs by design:
|
||||
|
||||
1. **The configuration.** Anyone who can supply or edit a YAML config is trusted
|
||||
(see below).
|
||||
2. **Authenticated peers of a running device** — clients holding the device's
|
||||
API encryption key / password, OTA password, or web server credentials.
|
||||
|
||||
The security boundary is therefore **unauthenticated network traffic vs. those
|
||||
trusted inputs.** A bug that lets an unauthenticated attacker cross it is a
|
||||
security bug.
|
||||
|
||||
## Config authors are host-equivalent by design
|
||||
|
||||
Anyone who can supply or edit a configuration is **trusted with full code
|
||||
execution on the host that runs `esphome`**, on purpose. This is what the product
|
||||
does, not a flaw. A config author can already, through fully supported features:
|
||||
|
||||
- Run arbitrary **Python** at validation/compile time via `external_components:`
|
||||
(and other component-import mechanisms) — ESPHome imports those packages as
|
||||
ordinary Python.
|
||||
- Run arbitrary **shell** commands through the compile/validate/flash toolchain
|
||||
that ESPHome invokes as subprocesses.
|
||||
- Read and write arbitrary files reachable by the process (e.g. via `!include`,
|
||||
`packages:`, `dashboard_import:`, and generated build output).
|
||||
|
||||
Because of this, a malicious config author is equivalent to shell access on the
|
||||
host running the build.
|
||||
|
||||
## What is *not* a security vulnerability
|
||||
|
||||
If exploiting an issue requires the ability to supply or edit configuration, it
|
||||
is **not** a vulnerability in ESPHome, because that ability already grants host
|
||||
code execution. This explicitly includes, among others:
|
||||
|
||||
- Template / expression injection in substitutions or any YAML string value
|
||||
(e.g. Jinja `${...}` evaluation reaching Python internals). This grants no
|
||||
capability a config author lacks.
|
||||
- `!include` / `packages:` / `dashboard_import:` reading or fetching content
|
||||
from surprising or remote locations.
|
||||
- The validator or compiler crashing or behaving unexpectedly on adversarial
|
||||
YAML.
|
||||
- ESPHome running as root in the official container — that is the documented
|
||||
deployment posture, reachable by the same caller through the features above.
|
||||
|
||||
These do not warrant a CVE or coordinated disclosure. Hardening in these areas
|
||||
(for example, sandboxing template evaluation as least-surprise defense-in-depth)
|
||||
is welcome as a normal enhancement PR, framed as cleanliness rather than a
|
||||
security fix — not as a vulnerability remediation.
|
||||
|
||||
## What we do defend
|
||||
|
||||
These *are* security bugs in this repo, and we want to hear about them privately:
|
||||
|
||||
- Memory-safety or protocol bugs in the generated **device firmware** that are
|
||||
remotely triggerable over the network (native API, web server, OTA, BLE,
|
||||
captive portal, etc.) **without** valid credentials.
|
||||
- Authentication or encryption bypass on the device — reaching API calls, OTA
|
||||
updates, or the web server without the configured key/password.
|
||||
- Flaws that weaken the device's API encryption (Noise), OTA, or web server auth
|
||||
below their documented guarantees.
|
||||
|
||||
## Explicitly out of scope
|
||||
|
||||
- Local attackers who already have shell access on the host that runs `esphome`.
|
||||
- Supply-chain attacks against ESPHome or its dependencies.
|
||||
- Operator-supplied hostile YAML (covered above — config authoring is trusted).
|
||||
- Attacks that require an already-authenticated device peer (someone who already
|
||||
holds the API key / OTA / web credentials).
|
||||
- Anything in the dashboard / device-builder — report that in its own repository
|
||||
(linked at the top).
|
||||
- The legacy bundled dashboard in this repo (`esphome/dashboard/`) — it is
|
||||
deprecated and being replaced by Device Builder; report dashboard issues there.
|
||||
- Deployments where the operator removed protections or exposed credentials. See
|
||||
the security best practices guide:
|
||||
https://esphome.io/guides/security_best_practices/
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
If you believe you've found an issue that crosses the unauthenticated boundary
|
||||
above, please report it privately via GitHub Security Advisories rather than a
|
||||
public issue. For issues that require config-write access, please review this
|
||||
document first — they are very likely out of scope by design. For dashboard /
|
||||
device-builder issues, report against that repository and consult its threat
|
||||
model (linked at the top).
|
||||
+1
-1
@@ -32,7 +32,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.9
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.12
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -49,7 +49,21 @@ if bashio::fs.directory_exists '/config/esphome/.esphome'; then
|
||||
rm -rf /config/esphome/.esphome
|
||||
fi
|
||||
|
||||
# Only signal device-builder to expose the public LAN port when the operator
|
||||
# mapped port 6052, matching the legacy dashboard where nginx listened on the
|
||||
# fixed port 6052 only when it was configured. We use the mapping purely as a
|
||||
# presence check and don't forward the published value; device-builder binds
|
||||
# its default port 6052 (the fixed container port, as the legacy
|
||||
# "listen 6052" did). --ha-addon-allow-public is inert on its own: the no-auth
|
||||
# gate is the DISABLE_HA_AUTHENTICATION env var set above, so both opt-ins are
|
||||
# required to bind 6052 unauthenticated; either alone stays ingress-only.
|
||||
set --
|
||||
if bashio::var.has_value "$(bashio::addon.port 6052)"; then
|
||||
set -- --ha-addon-allow-public
|
||||
fi
|
||||
|
||||
bashio::log.info "Starting ESPHome Device Builder..."
|
||||
exec esphome-device-builder /config/esphome \
|
||||
--ha-addon \
|
||||
--ingress-port "$(bashio::addon.ingress_port)"
|
||||
--ingress-port "$(bashio::addon.ingress_port)" \
|
||||
"$@"
|
||||
|
||||
@@ -50,6 +50,11 @@ async def new_fastled_light(config):
|
||||
ref="d44c800a9e876a8394caefc2ce4915dd96dac77b",
|
||||
)
|
||||
cg.add_library("SPI", None)
|
||||
# FastLED's RMT5 driver hard-codes intr_priority=3, which conflicts with
|
||||
# esphome's RMT channels (remote_transmitter etc., priority 0): the IDF
|
||||
# driver rejects FastLED's channel and show() then hangs ~3s with no
|
||||
# output. Override to 0 so it shares the interrupt. See #17063.
|
||||
cg.add_build_flag("-DFL_RMT5_INTERRUPT_LEVEL=0")
|
||||
else:
|
||||
cg.add_library("fastled/FastLED", "3.9.16")
|
||||
await light.register_light(var, config)
|
||||
|
||||
@@ -69,7 +69,7 @@ ENCRYPTION_SCHEMA = {
|
||||
cv.Optional(CONF_ENCRYPTION): cv.maybe_simple_value(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_KEY): cv.string,
|
||||
cv.Required(CONF_KEY): cv.sensitive(cv.string),
|
||||
}
|
||||
),
|
||||
key=CONF_KEY,
|
||||
|
||||
@@ -27,10 +27,6 @@ void Select::publish_state(size_t index) {
|
||||
const char *option = this->option_at(index);
|
||||
this->set_has_state(true);
|
||||
this->active_index_ = index;
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
this->state = option; // Update deprecated member for backward compatibility
|
||||
#pragma GCC diagnostic pop
|
||||
ESP_LOGV(TAG, "'%s' >> %s (%zu)", this->get_name().c_str(), option, index);
|
||||
this->state_callback_.call(index);
|
||||
#if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY)
|
||||
|
||||
@@ -30,15 +30,8 @@ class Select : public EntityBase {
|
||||
public:
|
||||
SelectTraits traits;
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
/// @deprecated Use current_option() instead. This member will be removed in ESPHome 2026.7.0.
|
||||
ESPDEPRECATED("Use current_option() instead of .state. Will be removed in 2026.7.0", "2026.1.0")
|
||||
std::string state{};
|
||||
|
||||
Select() = default;
|
||||
~Select() = default;
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
void publish_state(const std::string &state);
|
||||
void publish_state(const char *state);
|
||||
|
||||
@@ -32,7 +32,7 @@ from esphome.const import (
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@martgras", "@mebner86", "@mikelawrence", "@tuct"]
|
||||
CODEOWNERS = ["@martgras", "@mebner86", "@tuct"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
|
||||
@@ -81,8 +81,13 @@ def _get_idf_tools_path() -> Path:
|
||||
Path object pointing to the ESP-IDF tools directory
|
||||
"""
|
||||
if "ESPHOME_ESP_IDF_PREFIX" in os.environ:
|
||||
return Path(get_str_env("ESPHOME_ESP_IDF_PREFIX", None)).expanduser()
|
||||
return CORE.data_dir / "idf"
|
||||
path = Path(get_str_env("ESPHOME_ESP_IDF_PREFIX", None)).expanduser()
|
||||
else:
|
||||
path = CORE.data_dir / "idf"
|
||||
# Resolve so an unnormalized config path (e.g. compiling ``../config/x.yaml``)
|
||||
# doesn't leave ``..`` segments in the IDF_TOOLS_PATH handed to idf.py, which
|
||||
# otherwise warns that the venv interpreter path doesn't match the install.
|
||||
return path.resolve()
|
||||
|
||||
|
||||
# Windows' default MAX_PATH is 260 characters. ESP-IDF toolchains nest deeply
|
||||
|
||||
+2
-2
@@ -19,13 +19,13 @@ ruamel.yaml==0.19.1 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.15 # dashboard_import
|
||||
esphome-glyphsets==0.2.0
|
||||
pillow==12.2.0
|
||||
resvg-py==0.3.2
|
||||
resvg-py==0.3.3
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
bleak==2.1.1
|
||||
smpclient==6.0.0
|
||||
requests==2.34.2
|
||||
py7zr==1.1.0
|
||||
py7zr==1.1.3
|
||||
|
||||
# esp-idf >= 5.0 requires this
|
||||
pyparsing >= 3.3.2
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
pylint==4.0.6
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.17 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.18 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
# Unit tests
|
||||
pytest==9.1.0
|
||||
pytest==9.1.1
|
||||
pytest-cov==7.1.0
|
||||
pytest-mock==3.15.1
|
||||
pytest-asyncio==1.4.0
|
||||
|
||||
@@ -109,7 +109,7 @@ select:
|
||||
set_action:
|
||||
- lambda: |-
|
||||
ESP_LOGI("test", "Device A Mode set to %s", x.c_str());
|
||||
id(mode_device_a).state = x;
|
||||
id(mode_device_a).publish_state(x);
|
||||
|
||||
- platform: template
|
||||
name: Mode
|
||||
@@ -124,7 +124,7 @@ select:
|
||||
set_action:
|
||||
- lambda: |-
|
||||
ESP_LOGI("test", "Device B Mode set to %s", x.c_str());
|
||||
id(mode_device_b).state = x;
|
||||
id(mode_device_b).publish_state(x);
|
||||
|
||||
- platform: template
|
||||
name: Mode
|
||||
@@ -138,7 +138,7 @@ select:
|
||||
set_action:
|
||||
- lambda: |-
|
||||
ESP_LOGI("test", "Main Mode set to %s", x.c_str());
|
||||
id(mode_main).state = x;
|
||||
id(mode_main).publish_state(x);
|
||||
|
||||
# Button to trigger preference logging test
|
||||
button:
|
||||
@@ -153,9 +153,9 @@ button:
|
||||
ESP_LOGI("test", "Device A Setpoint: %.1f", id(setpoint_device_a).state);
|
||||
ESP_LOGI("test", "Device B Setpoint: %.1f", id(setpoint_device_b).state);
|
||||
ESP_LOGI("test", "Main Setpoint: %.1f", id(setpoint_main).state);
|
||||
ESP_LOGI("test", "Device A Mode: %s", id(mode_device_a).state.c_str());
|
||||
ESP_LOGI("test", "Device B Mode: %s", id(mode_device_b).state.c_str());
|
||||
ESP_LOGI("test", "Main Mode: %s", id(mode_main).state.c_str());
|
||||
ESP_LOGI("test", "Device A Mode: %s", id(mode_device_a).current_option().c_str());
|
||||
ESP_LOGI("test", "Device B Mode: %s", id(mode_device_b).current_option().c_str());
|
||||
ESP_LOGI("test", "Main Mode: %s", id(mode_main).current_option().c_str());
|
||||
// Log preference hashes for entities that actually store preferences
|
||||
ESP_LOGI("test", "Device A Switch Pref Hash: %u", id(light_device_a).get_preference_hash());
|
||||
ESP_LOGI("test", "Device B Switch Pref Hash: %u", id(light_device_b).get_preference_hash());
|
||||
|
||||
Reference in New Issue
Block a user