From ad1b6ab37e501db1b5d02d7830518da70cdac0af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Oct 2026 18:23:36 -0500 Subject: [PATCH] [web_server] Keep allowed origins in a shared flash table (#20086) --- esphome/components/web_server/__init__.py | 11 ++++++++++- esphome/components/web_server/web_server.cpp | 3 ++- esphome/components/web_server/web_server.h | 6 +++--- .../web_server/config/allowed_origins.yaml | 15 +++++++++++++++ .../web_server/test_allowed_origins.py | 18 ++++++++++++++++++ 5 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 tests/component_tests/web_server/config/allowed_origins.yaml create mode 100644 tests/component_tests/web_server/test_allowed_origins.py diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 2b00a6f8de..6bc4d7ab5c 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -413,7 +413,16 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS") if (allowed_origins := config.get(CONF_ALLOWED_ORIGINS)) is not None: cg.add_define("USE_WEBSERVER_ALLOWED_ORIGINS") - cg.add(var.set_allowed_origins(allowed_origins)) + # Shared flash table ended by nullptr, so the server stores only a pointer. + cg.add( + var.set_allowed_origins( + cg.shared_progmem_array( + "web_server_allowed_origins", + cg.const_char_ptr, + [*allowed_origins, cg.nullptr], + ) + ) + ) if (auth := config.get(CONF_AUTH)) is not None: cg.add_define("USE_WEBSERVER_AUTH") # The scheme is fixed at build time so the unused Basic/Digest code path is compiled diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 74641738ec..90c6b167f6 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -495,7 +495,8 @@ bool WebServer::is_request_origin_allowed_(AsyncWebServerRequest *request, const #ifdef USE_WEBSERVER_ALLOWED_ORIGINS // Otherwise the origin must be explicitly allowed via configuration. - for (const char *allowed_origin : this->allowed_origins_) { + for (const char *const *it = this->allowed_origins_; *it != nullptr; it++) { + const char *allowed_origin = *it; // A single "*" entry allows any origin. if (allowed_origin[0] == '*' && allowed_origin[1] == '\0') return true; diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 6f08a8dc8f..1f9b6cb934 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -254,9 +254,9 @@ class WebServer final : public Component, public AsyncWebHandler { * * This list is also used to authorize Private Network Access requests when that feature is enabled. * - * @param origins The list of allowed origins. + * @param origins nullptr-terminated table of allowed origins; must outlive the server. */ - void set_allowed_origins(std::initializer_list origins) { this->allowed_origins_ = origins; } + void set_allowed_origins(const char *const *origins) { this->allowed_origins_ = origins; } #endif // ========== INTERNAL METHODS ========== @@ -620,7 +620,7 @@ class WebServer final : public Component, public AsyncWebHandler { #ifdef USE_WEBSERVER_ALLOWED_ORIGINS // Extra origins allowed to make cross-origin browser requests ("*" means any origin). // Only compiled when allowed_origins is configured; same-origin is always allowed regardless. - FixedVector allowed_origins_; + const char *const *allowed_origins_{nullptr}; #endif /// Check whether the given request Origin is permitted. Same-origin (matching the Host the diff --git a/tests/component_tests/web_server/config/allowed_origins.yaml b/tests/component_tests/web_server/config/allowed_origins.yaml new file mode 100644 index 0000000000..2ced49d6d8 --- /dev/null +++ b/tests/component_tests/web_server/config/allowed_origins.yaml @@ -0,0 +1,15 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ssid: test + password: testtest + +web_server: + allowed_origins: + - https://app.esphome.io + - "*" diff --git a/tests/component_tests/web_server/test_allowed_origins.py b/tests/component_tests/web_server/test_allowed_origins.py new file mode 100644 index 0000000000..7a6c602c12 --- /dev/null +++ b/tests/component_tests/web_server/test_allowed_origins.py @@ -0,0 +1,18 @@ +"""Tests for the web_server allowed_origins codegen.""" + +from collections.abc import Callable +from pathlib import Path + + +def test_allowed_origins_are_a_progmem_table( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Allowed origins live in a nullptr-terminated flash table instead of a heap vector.""" + main_cpp = generate_main(component_config_path("allowed_origins.yaml")) + + assert ( + "static constexpr const char * web_server_allowed_origins[] PROGMEM = " + '{"https://app.esphome.io", "*", nullptr};' in main_cpp + ) + assert "->set_allowed_origins(web_server_allowed_origins);" in main_cpp