From cd6a34401ba52255774dfa8e4eb25422f083f4a1 Mon Sep 17 00:00:00 2001 From: sebo-b <51707418+sebo-b@users.noreply.github.com> Date: Fri, 16 Oct 2026 20:22:46 +0200 Subject: [PATCH] [web_server] Do not match internal entities by URL unless include_internal is set (#20883) Co-authored-by: Claude Opus 5.5 Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/web_server/web_server.cpp | 5 +++++ esphome/components/web_server/web_server.h | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index b0da010561e..04cd0ff43d2 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -203,6 +203,10 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain, EntityMatchResult UrlMatch::match_entity(EntityBase *entity) const { EntityMatchResult result{false, this->method.empty()}; + // Internal entities are unreachable by URL unless include_internal is set, as with the native API and MQTT. + if (!this->include_internal && entity->is_internal()) + return result; + #ifdef USE_DEVICES Device *entity_device = entity->get_device(); bool url_has_device = !this->device_name.empty(); @@ -2560,6 +2564,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { // Parse URL for component routing // Pass HTTP method to disambiguate 3-segment URLs (GET=sub-device state, POST=main device action) UrlMatch match = match_url(url.c_str(), url.length(), false, request->method() == HTTP_POST); + match.include_internal = this->include_internal_; // Route to appropriate handler based on domain // NOLINTNEXTLINE(readability-simplify-boolean-expr) diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index d3bd1ae0da6..f8ef12f9bff 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -66,7 +66,8 @@ struct UrlMatch { #ifdef USE_DEVICES StringRef device_name; ///< Device name within URL, empty for main device #endif - bool valid{false}; ///< Whether this match is valid + bool valid{false}; ///< Whether this match is valid + bool include_internal{false}; ///< Whether internal entities may match, the web server's include_internal // Helper methods for string comparisons bool domain_equals(const char *str) const { return this->domain == str; } @@ -78,7 +79,7 @@ struct UrlMatch { bool method_equals(const __FlashStringHelper *str) const { return this->method == str; } #endif - /// Match entity by name + /// Match entity by name; internal entities match only if include_internal is set /// Returns EntityMatchResult with match status and whether action segment is empty EntityMatchResult match_entity(EntityBase *entity) const; };