THREAT_MODEL.md
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:
For this repository there are two trusted inputs by design:
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.
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:
external_components:
(and other component-import mechanisms) — ESPHome imports those packages as
ordinary Python.!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.
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:
${...} 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.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.
These are security bugs in this repo, and we want to hear about them privately:
The web_server component exposes a plain HTTP interface for viewing and
controlling entities, and, when the web_server OTA platform is enabled, for
uploading firmware at /update. Its only access controls are the optional
web_server auth: credentials and the network the device sits on.
When auth: is not configured, every endpoint is reachable by any client that
can reach the device. This is intentional; enabling web_server without auth:
is choosing an open control surface, in the same way that running native OTA
without a password leaves OTA open. The API is documented and is meant to be
called by other devices, scripts, and pages.
As defense-in-depth, the web server checks the Origin header on browser requests
to its entity control and state endpoints: a request whose Origin does not match
the address the device is served on is rejected, and the allowed_origins option
widens that list. This blocks the common "confused deputy" (CSRF) case where a page
the operator visits drives the device through their browser. It is not an
authentication boundary: it only constrains browsers. Any client that omits the
Origin header — curl, scripts, or other non-browser callers on the same
network — reaches every endpoint exactly as before. The check also does not cover
the web OTA /update endpoint. The device performs no CSRF-token or Referer
validation. The following are therefore not vulnerabilities in this repository:
Origin header (for example curl) reaching the control
endpoints, whether or not web_server auth: is set.allowed_origins./update) when
web OTA is enabled without web_server auth:. The /update endpoint is not
covered by the Origin check; this is the same exposure as running OTA without a
password.The supported defenses are web_server auth:, protecting OTA (a web password or
a native OTA password), and keeping devices on a trusted, segmented network. See
the security best practices guide linked above.
What remains in scope is bypassing web_server auth: when it is configured,
and any memory-safety or protocol bug in the server reachable without credentials.
This section documents the current design and scope; it is not a judgment that the design is optimal or that it will not change.
The esphome OTA platform optionally encrypts updates with the same Noise
NNpsk0 pattern the native API uses; one key protects the device. A device
whose api: block has an encryption key, static in the YAML or provisioned at
runtime, compiles in the transport and offers it on every OTA connection once
it holds a key, so an uploader presenting that key gets the guarantees below
even without an ota: encryption: block; only that block makes the device
require encryption. The guarantees are: the firmware image is confidential in
transit, the uploader is authenticated by the pre-shared key, and the plaintext
negotiation preceding the handshake is bound into the handshake prologue, so
stripping or tampering with it fails the first MAC. With ota: encryption:
configured both ends fail closed with no override: the device refuses
plaintext uploads, and the CLI refuses to send plaintext when a key is
configured. Without that block the CLI tries a static api key when the device
offers and, until 2027.3.0, falls back to plaintext with a warning when the
offer is missing or the handshake fails; a runtime provisioned key never
reaches the CLI, so those uploads stay plaintext.
Defeating any of that without the key is in scope: a device that requires encryption accepting a plaintext or downgraded upload, getting past the MAC, or recovering image contents from captured traffic.
The following are not vulnerabilities, by design:
ota: encryption: block, including one
that offers encryption because it has an api key. That is the documented
default, authenticated (if at all) by the OTA password. An uploader that
takes the offer skips the password; the key authenticates it. With a
runtime provisioned key and no provisioning: window, whoever provisions
the key gains that upload path too; validation warns about the pair.ota: encryption: an
active attacker who strips the offer or breaks the handshake can make a
keyed CLI upload plaintext, with the pre-existing plaintext exposure. A
device that requires encryption still refuses that upload.ota: encryption: is itself an encrypted upload.
Older firmware needs one last plaintext upload of an offering build, with
the pre-existing plaintext exposure./update endpoint alongside encryption. With the web_server
or prometheus component the shared listener is always up, so the endpoint
stays reachable and validation warns about that combination;
captive_portal: alone brings the listener up only for the fallback AP
window, which is the intended recovery path, so that is not warned about.esphome.Origin header). The web server is an open HTTP API by
design (see above); browser cross-origin requests are blocked by default, but the
real controls are web_server auth: and network isolation.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).