From 2fe9de7dbca247480b7cf2dbdd2018b38a46efc7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 11:22:31 -1000 Subject: [PATCH] add max length validation for device class strings Defense-in-depth: validate device class strings don't exceed the 48-byte PROGMEM buffer limit (47 chars + null), matching the same pattern used for icon strings. --- esphome/config_validation.py | 4 ++++ esphome/core/entity_helpers.py | 4 ++++ tests/unit_tests/core/test_entity_helpers.py | 17 +++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 368b4f9f4a..b5b5189c7c 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -398,6 +398,10 @@ def string_strict(value): ) +# Max device class string length (47 chars + null = 48-byte PROGMEM buffer) +# Keep in sync with MAX_DEVICE_CLASS_LENGTH in esphome/core/entity_base.h +DEVICE_CLASS_MAX_LENGTH = 47 + # Max icon string length (63 chars + null = 64-byte PROGMEM buffer) # Keep in sync with MAX_ICON_LENGTH in esphome/core/entity_base.h ICON_MAX_LENGTH = 63 diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 7f112bf3bf..f45a9dae7a 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -178,6 +178,10 @@ def _register_string( def register_device_class(value: str) -> int: """Register a device_class string and return its 1-based index.""" + if value and len(value) > cv.DEVICE_CLASS_MAX_LENGTH: + raise ValueError( + f"Device class string too long ({len(value)} chars, max {cv.DEVICE_CLASS_MAX_LENGTH}): '{value}'" + ) return _register_string( value, _get_pool().device_classes, _MAX_DEVICE_CLASSES, "device_class" ) diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 79bc3095b9..1392a1d043 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -23,6 +23,7 @@ from esphome.core.entity_helpers import ( _setup_entity_impl, entity_duplicate_validator, get_base_entity_object_id, + register_device_class, register_icon, setup_entity, ) @@ -926,6 +927,22 @@ def test_register_icon_max_length() -> None: assert register_icon("") == 0 +def test_register_device_class_max_length() -> None: + """Test register_device_class rejects device classes exceeding 47 characters.""" + # 47 chars should succeed + max_dc = "a" * 47 + idx = register_device_class(max_dc) + assert idx > 0 + + # 48 chars should fail + too_long = "a" * 48 + with pytest.raises(ValueError, match="Device class string too long"): + register_device_class(too_long) + + # Empty string returns 0 + assert register_device_class("") == 0 + + @pytest.mark.asyncio async def test_setup_entity_with_entity_category( setup_test_environment: list[str],