From 7d5690b881b7b7667dbea323118cc0d41d5301f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Sep 2026 13:24:19 +0200 Subject: [PATCH] Only warn about the web_server update endpoint when web_server is configured --- THREAT_MODEL.md | 6 ++-- esphome/components/esphome/ota/__init__.py | 32 +++++++------------ tests/component_tests/ota/test_esphome_ota.py | 11 ++++--- 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index b716fe9ca2..2ff6437ecf 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -152,9 +152,9 @@ The following are **not** vulnerabilities, by design: Older firmware needs one last plaintext upload of an offering build, with the pre-existing plaintext exposure. - The web OTA `/update` endpoint alongside encryption. The `web_server` - component keeps it always reachable, and `captive_portal:` auto-loads it - for the fallback AP window; validation warns about both combinations, and - the operator keeps the recovery path. + component keeps it always reachable and validation warns about that + combination; `captive_portal:` auto-loads it only for the fallback AP + window, which is the intended recovery path, so that is not warned about. - CLI retry behavior on transport or MAC failures; every attempt renegotiates a fresh handshake with fresh ephemerals, so retrying does not weaken authentication. diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index dbbaa6ef48..1f04008b94 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -27,7 +27,6 @@ import esphome.final_validate as fv from esphome.types import ConfigType CONF_ALLOW_PARTITION_ACCESS = "allow_partition_access" -CONF_CAPTIVE_PORTAL = "captive_portal" _LOGGER = logging.getLogger(__name__) @@ -146,12 +145,13 @@ def ota_esphome_final_validate(config: ConfigType) -> None: CONF_ENCRYPTION, CONF_OTA, ) - if any( - conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf - ) and any( + # The captive_portal auto-loads the web_server ota platform too, but that + # endpoint only exists while the fallback AP is active and is the intended + # recovery path, so only an explicit web_server component warns + if CONF_WEB_SERVER in full_conf and any( CONF_ENCRYPTION in conf for conf in merged_ota_esphome_configs_by_port.values() ): - _warn_web_server_ota(full_conf) + _warn_web_server_ota() full_conf[CONF_OTA] = new_ota_conf fv.full_config.set(full_conf) @@ -166,25 +166,15 @@ def ota_esphome_final_validate(config: ConfigType) -> None: ) -def _warn_web_server_ota(full_conf: ConfigType) -> None: +def _warn_web_server_ota() -> None: """The web_server ota platform accepts the same image over plaintext HTTP with basic auth, bypassing the encryption; warn rather than fail so the operator keeps the recovery path.""" - if CONF_CAPTIVE_PORTAL in full_conf and CONF_WEB_SERVER not in full_conf: - # The captive_portal auto-load: the endpoint only exists while the - # fallback AP is active - _LOGGER.warning( - "OTA encryption does not cover the %s OTA platform (auto-loaded " - "by captive_portal); the plaintext /update endpoint stays " - "reachable while the fallback AP is active", - CONF_WEB_SERVER, - ) - else: - _LOGGER.warning( - "OTA encryption does not cover the %s OTA platform; its " - "plaintext /update endpoint accepts the same image", - CONF_WEB_SERVER, - ) + _LOGGER.warning( + "OTA encryption does not cover the %s OTA platform; its " + "plaintext /update endpoint accepts the same image", + CONF_WEB_SERVER, + ) def _api_static_key(api_conf: ConfigType) -> str | None: diff --git a/tests/component_tests/ota/test_esphome_ota.py b/tests/component_tests/ota/test_esphome_ota.py index e88d4efaca..26d22472f7 100644 --- a/tests/component_tests/ota/test_esphome_ota.py +++ b/tests/component_tests/ota/test_esphome_ota.py @@ -318,12 +318,12 @@ def test_encryption_with_web_server_ota_warns( fv.full_config.reset(token) -def test_encryption_with_captive_portal_web_server_ota_warns( +def test_encryption_with_captive_portal_does_not_warn( caplog: pytest.LogCaptureFixture, ) -> None: """captive_portal auto-loads the web_server ota platform without the - web_server component; encryption stays usable and only warns, so the - fallback AP recovery path is not lost.""" + web_server component; its endpoint only exists while the fallback AP is + active and is the intended recovery path, so there is no warning.""" full_conf = { "captive_portal": {}, CONF_OTA: [ @@ -335,7 +335,10 @@ def test_encryption_with_captive_portal_web_server_ota_warns( try: with caplog.at_level(logging.WARNING): ota_esphome_final_validate({}) - assert any("captive_portal" in record.message for record in caplog.records) + assert not any( + "OTA encryption does not cover" in record.message + for record in caplog.records + ) esphome_conf = next( conf for conf in fv.full_config.get()[CONF_OTA]