From 67c8312c03b703cfab871a89b105130e690cbec1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Sep 2026 17:11:27 +0200 Subject: [PATCH] Keep secret values out of configuration error messages (#183760) --- .../components/apple_tv/config_flow.py | 4 ++- homeassistant/components/besen/config_flow.py | 10 +++++-- homeassistant/components/blink/config_flow.py | 6 +++- .../components/braviatv/config_flow.py | 4 +-- .../components/comelit/config_flow.py | 6 ++-- .../components/comfoconnect/__init__.py | 4 ++- .../eurotronic_cometblue/config_flow.py | 2 +- homeassistant/components/fints/sensor.py | 2 +- homeassistant/components/fumis/config_flow.py | 8 +++--- .../husqvarna_automower_ble/config_flow.py | 4 +-- .../components/motionmount/config_flow.py | 2 +- .../nintendo_parental_controls/services.py | 2 +- .../components/panasonic_viera/config_flow.py | 4 ++- .../components/philips_js/config_flow.py | 2 +- homeassistant/components/risco/config_flow.py | 4 +-- .../components/samsungtv/config_flow.py | 8 ++++-- .../components/subaru/config_flow.py | 2 +- homeassistant/components/venstar/climate.py | 2 +- .../components/venstar/config_flow.py | 2 +- homeassistant/components/vizio/config_flow.py | 6 +++- .../components/worxlandroid/sensor.py | 2 +- homeassistant/config.py | 14 ++++++---- tests/test_config.py | 28 +++++++++++++++++++ 23 files changed, 91 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 198346bda80e..06a7443ad324 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -42,7 +42,9 @@ _LOGGER = logging.getLogger(__name__) DEVICE_INPUT = "device_input" -INPUT_PIN_SCHEMA = probatio.Schema({probatio.Required(CONF_PIN, default=""): cv.string}) +INPUT_PIN_SCHEMA = probatio.Schema( + {probatio.Required(probatio.Secret(CONF_PIN), default=""): cv.string} +) DEFAULT_START_OFF = False diff --git a/homeassistant/components/besen/config_flow.py b/homeassistant/components/besen/config_flow.py index 0745ebd5fded..ec2a828a2ba8 100644 --- a/homeassistant/components/besen/config_flow.py +++ b/homeassistant/components/besen/config_flow.py @@ -43,11 +43,13 @@ PIN_SCHEMA = selector.TextSelector( PIN_ONLY_SCHEMA = probatio.Schema( { - probatio.Required(CONF_PIN, default=DEFAULT_PIN): PIN_SCHEMA, + probatio.Required(probatio.Secret(CONF_PIN), default=DEFAULT_PIN): PIN_SCHEMA, } ) -REAUTH_SCHEMA = probatio.Schema({probatio.Required(CONF_PIN): PIN_SCHEMA}) +REAUTH_SCHEMA = probatio.Schema( + {probatio.Required(probatio.Secret(CONF_PIN)): PIN_SCHEMA} +) def _user_schema( @@ -63,7 +65,9 @@ def _user_schema( for address, discovery in discoveries.items() } ), - probatio.Required(CONF_PIN, default=DEFAULT_PIN): PIN_SCHEMA, + probatio.Required( + probatio.Secret(CONF_PIN), default=DEFAULT_PIN + ): PIN_SCHEMA, } ) diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index e388ffb755e0..16c46610c1ae 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -117,7 +117,11 @@ class BlinkConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="2fa", data_schema=probatio.Schema( - {probatio.Optional(CONF_PIN): probatio.All(str, probatio.Length(min=1))} + { + probatio.Optional(probatio.Secret(CONF_PIN)): probatio.All( + str, probatio.Length(min=1) + ) + } ), errors=errors, ) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 9ca7748953c8..b5bf211fad44 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -181,7 +181,7 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN): step_id="pin", data_schema=probatio.Schema( { - probatio.Required(CONF_PIN): str, + probatio.Required(probatio.Secret(CONF_PIN)): str, } ), errors=errors, @@ -210,7 +210,7 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN): step_id="psk", data_schema=probatio.Schema( { - probatio.Required(CONF_PIN): str, + probatio.Required(probatio.Secret(CONF_PIN)): str, } ), errors=errors, diff --git a/homeassistant/components/comelit/config_flow.py b/homeassistant/components/comelit/config_flow.py index 1f1b32a8683d..f3926bfaf690 100644 --- a/homeassistant/components/comelit/config_flow.py +++ b/homeassistant/components/comelit/config_flow.py @@ -30,14 +30,14 @@ USER_SCHEMA = probatio.Schema( { probatio.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, probatio.Required(CONF_PORT, default=DEFAULT_PORT): probatio.Port(), - probatio.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string, + probatio.Optional(probatio.Secret(CONF_PIN), default=DEFAULT_PIN): cv.string, probatio.Required(CONF_TYPE, default=BRIDGE): probatio.In(DEVICE_TYPE_LIST), probatio.Optional(CONF_VEDO_PIN): cv.string, } ) STEP_REAUTH_DATA_SCHEMA = probatio.Schema( { - probatio.Required(CONF_PIN): cv.string, + probatio.Required(probatio.Secret(CONF_PIN)): cv.string, probatio.Optional(CONF_VEDO_PIN): cv.string, } ) @@ -239,7 +239,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): probatio.Required( CONF_PORT, default=reconfigure_entry.data[CONF_PORT] ): probatio.Port(), - probatio.Optional(CONF_PIN): cv.string, + probatio.Optional(probatio.Secret(CONF_PIN)): cv.string, probatio.Optional(CONF_VEDO_PIN): cv.string, } ) diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index 5d9be30787fc..f24ccfb008b8 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -43,7 +43,9 @@ CONFIG_SCHEMA = probatio.Schema( probatio.Optional( CONF_USER_AGENT, default=DEFAULT_USER_AGENT ): cv.string, - probatio.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int, + probatio.Optional( + probatio.Secret(CONF_PIN), default=DEFAULT_PIN + ): cv.positive_int, } ) }, diff --git a/homeassistant/components/eurotronic_cometblue/config_flow.py b/homeassistant/components/eurotronic_cometblue/config_flow.py index f140bdd7667a..69c0ce24a237 100644 --- a/homeassistant/components/eurotronic_cometblue/config_flow.py +++ b/homeassistant/components/eurotronic_cometblue/config_flow.py @@ -29,7 +29,7 @@ LOGGER = logging.getLogger(__name__) DATA_SCHEMA = probatio.Schema( { - probatio.Required(CONF_PIN, default="000000"): probatio.All( + probatio.Required(probatio.Secret(CONF_PIN), default="000000"): probatio.All( TextSelector(TextSelectorConfig(type=TextSelectorType.NUMBER)), probatio.Length(min=6, max=6), ), diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 2841ade3d139..983801db29d6 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -48,7 +48,7 @@ PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { probatio.Required(CONF_BIN): cv.string, probatio.Required(CONF_USERNAME): cv.string, - probatio.Required(CONF_PIN): cv.string, + probatio.Required(probatio.Secret(CONF_PIN)): cv.string, probatio.Required(CONF_URL): cv.string, probatio.Optional(CONF_NAME): cv.string, probatio.Optional(CONF_ACCOUNTS, default=[]): probatio.EnsureList()( diff --git a/homeassistant/components/fumis/config_flow.py b/homeassistant/components/fumis/config_flow.py index 298b4dddfa1c..8328ad2a77b2 100644 --- a/homeassistant/components/fumis/config_flow.py +++ b/homeassistant/components/fumis/config_flow.py @@ -67,7 +67,7 @@ class FumisFlowHandler(ConfigFlow, domain=DOMAIN): step_id="dhcp_confirm", data_schema=probatio.Schema( { - probatio.Required(CONF_PIN): TextSelector( + probatio.Required(probatio.Secret(CONF_PIN)): TextSelector( TextSelectorConfig(type=TextSelectorType.PASSWORD) ), } @@ -104,7 +104,7 @@ class FumisFlowHandler(ConfigFlow, domain=DOMAIN): probatio.Required(CONF_MAC): TextSelector( TextSelectorConfig(autocomplete="off") ), - probatio.Required(CONF_PIN): TextSelector( + probatio.Required(probatio.Secret(CONF_PIN)): TextSelector( TextSelectorConfig(type=TextSelectorType.PASSWORD) ), } @@ -135,7 +135,7 @@ class FumisFlowHandler(ConfigFlow, domain=DOMAIN): step_id="reconfigure", data_schema=probatio.Schema( { - probatio.Required(CONF_PIN): TextSelector( + probatio.Required(probatio.Secret(CONF_PIN)): TextSelector( TextSelectorConfig(type=TextSelectorType.PASSWORD) ), } @@ -170,7 +170,7 @@ class FumisFlowHandler(ConfigFlow, domain=DOMAIN): step_id="reauth_confirm", data_schema=probatio.Schema( { - probatio.Required(CONF_PIN): TextSelector( + probatio.Required(probatio.Secret(CONF_PIN)): TextSelector( TextSelectorConfig(type=TextSelectorType.PASSWORD) ), } diff --git a/homeassistant/components/husqvarna_automower_ble/config_flow.py b/homeassistant/components/husqvarna_automower_ble/config_flow.py index df0bdc64cfd4..6757ee2a67f7 100644 --- a/homeassistant/components/husqvarna_automower_ble/config_flow.py +++ b/homeassistant/components/husqvarna_automower_ble/config_flow.py @@ -22,14 +22,14 @@ from .const import DOMAIN, LOGGER BLUETOOTH_SCHEMA = probatio.Schema( { - probatio.Required(CONF_PIN): str, + probatio.Required(probatio.Secret(CONF_PIN)): str, } ) USER_SCHEMA = probatio.Schema( { probatio.Required(CONF_ADDRESS): str, - probatio.Required(CONF_PIN): str, + probatio.Required(probatio.Secret(CONF_PIN)): str, } ) diff --git a/homeassistant/components/motionmount/config_flow.py b/homeassistant/components/motionmount/config_flow.py index 71cc43a1589b..e8a4ebcef5df 100644 --- a/homeassistant/components/motionmount/config_flow.py +++ b/homeassistant/components/motionmount/config_flow.py @@ -202,7 +202,7 @@ class MotionMountFlowHandler(ConfigFlow, domain=DOMAIN): step_id="auth", data_schema=probatio.Schema( { - probatio.Required(CONF_PIN): probatio.All( + probatio.Required(probatio.Secret(CONF_PIN)): probatio.All( int, probatio.Range(min=1, max=9999) ), } diff --git a/homeassistant/components/nintendo_parental_controls/services.py b/homeassistant/components/nintendo_parental_controls/services.py index ac9a467c35b9..b22e86ea3352 100644 --- a/homeassistant/components/nintendo_parental_controls/services.py +++ b/homeassistant/components/nintendo_parental_controls/services.py @@ -83,7 +83,7 @@ def async_setup_services( probatio.Schema( { probatio.Required(ATTR_DEVICE_ID): cv.string, - probatio.Required(CONF_PIN): cv.string, + probatio.Required(probatio.Secret(CONF_PIN)): cv.string, } ), ) diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index 4cb9ca487bb9..24f54606f80a 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -156,7 +156,9 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="pairing", - data_schema=probatio.Schema({probatio.Required(CONF_PIN): str}), + data_schema=probatio.Schema( + {probatio.Required(probatio.Secret(CONF_PIN)): str} + ), errors=errors, ) diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index a330850c05c8..59da9583196c 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -125,7 +125,7 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} schema = probatio.Schema( { - probatio.Required(CONF_PIN): str, + probatio.Required(probatio.Secret(CONF_PIN)): str, } ) diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index a2d72ee58149..21aacc3badc8 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -46,14 +46,14 @@ CLOUD_SCHEMA = probatio.Schema( { probatio.Required(CONF_USERNAME): str, probatio.Required(CONF_PASSWORD): str, - probatio.Required(CONF_PIN): str, + probatio.Required(probatio.Secret(CONF_PIN)): str, } ) LOCAL_SCHEMA = probatio.Schema( { probatio.Required(CONF_HOST): str, probatio.Required(CONF_PORT, default=1000): int, - probatio.Required(CONF_PIN): str, + probatio.Required(probatio.Secret(CONF_PIN)): str, } ) HA_STATES = [ diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 0abfd7695dbb..05bc7c4abc6f 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -391,7 +391,9 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): step_id="encrypted_pairing", errors=errors, description_placeholders={"device": self._title}, - data_schema=probatio.Schema({probatio.Required(CONF_PIN): str}), + data_schema=probatio.Schema( + {probatio.Required(probatio.Secret(CONF_PIN)): str} + ), ) @callback @@ -679,5 +681,7 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): step_id="reauth_confirm_encrypted", errors=errors, description_placeholders={"device": reauth_entry.title}, - data_schema=probatio.Schema({probatio.Required(CONF_PIN): str}), + data_schema=probatio.Schema( + {probatio.Required(probatio.Secret(CONF_PIN)): str} + ), ) diff --git a/homeassistant/components/subaru/config_flow.py b/homeassistant/components/subaru/config_flow.py index 33f5cf58b0cf..7d370fb87d4c 100644 --- a/homeassistant/components/subaru/config_flow.py +++ b/homeassistant/components/subaru/config_flow.py @@ -31,7 +31,7 @@ from .coordinator import SubaruConfigEntry _LOGGER = logging.getLogger(__name__) CONF_CONTACT_METHOD = "contact_method" CONF_VALIDATION_CODE = "validation_code" -PIN_SCHEMA = probatio.Schema({probatio.Required(CONF_PIN): str}) +PIN_SCHEMA = probatio.Schema({probatio.Required(probatio.Secret(CONF_PIN)): str}) class SubaruConfigFlow(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 4a9676324a23..4b1b4337ea75 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -61,7 +61,7 @@ PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( probatio.Coerce(int), probatio.Range(min=1) ), probatio.Optional(CONF_USERNAME): cv.string, - probatio.Optional(CONF_PIN): cv.string, + probatio.Optional(probatio.Secret(CONF_PIN)): cv.string, } ) diff --git a/homeassistant/components/venstar/config_flow.py b/homeassistant/components/venstar/config_flow.py index c10b162f8bb6..e57e6267bd3e 100644 --- a/homeassistant/components/venstar/config_flow.py +++ b/homeassistant/components/venstar/config_flow.py @@ -78,7 +78,7 @@ class VenstarConfigFlow(ConfigFlow, domain=DOMAIN): probatio.Required(CONF_HOST): str, probatio.Optional(CONF_USERNAME): str, probatio.Optional(CONF_PASSWORD): str, - probatio.Optional(CONF_PIN): str, + probatio.Optional(probatio.Secret(CONF_PIN)): str, probatio.Optional(CONF_SSL, default=False): bool, } ), diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 6c100e81961f..c4656f5db0f2 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -91,7 +91,11 @@ def _get_pairing_schema(input_dict: dict[str, Any] | None = None) -> probatio.Sc input_dict = {} return probatio.Schema( - {probatio.Required(CONF_PIN, default=input_dict.get(CONF_PIN, "")): str} + { + probatio.Required( + probatio.Secret(CONF_PIN), default=input_dict.get(CONF_PIN, "") + ): str + } ) diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 184312d281f1..198c52cce02f 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -27,7 +27,7 @@ DEFAULT_TIMEOUT = 5 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { probatio.Required(CONF_HOST): cv.string, - probatio.Required(CONF_PIN): cv.string, + probatio.Required(probatio.Secret(CONF_PIN)): cv.string, probatio.Optional(CONF_ALLOW_UNREACHABLE, default=True): cv.boolean, probatio.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, } diff --git a/homeassistant/config.py b/homeassistant/config.py index 6e14cd4f2896..a71c97e0b2b8 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -25,6 +25,7 @@ from .core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from .core_config import _PACKAGE_DEFINITION_SCHEMA, _PACKAGES_CONFIG_SCHEMA from .exceptions import ConfigValidationError, HomeAssistantError from .helpers import config_validation as cv +from .helpers.redact import REDACTED from .helpers.translation import async_get_exception_message from .helpers.typing import ConfigType from .loader import ComponentProtocol, Integration, IntegrationNotFound @@ -497,11 +498,14 @@ def stringify_invalid( output = Exception.__str__(exc) if error_type := exc.error_type: output += " for " + error_type - offending_item_summary = repr(_get_by_path(config, exc.path)) - if len(offending_item_summary) > max_sub_error_length: - offending_item_summary = ( - f"{offending_item_summary[: max_sub_error_length - 3]}..." - ) + if exc.secret: + offending_item_summary = REDACTED + else: + offending_item_summary = repr(_get_by_path(config, exc.path)) + if len(offending_item_summary) > max_sub_error_length: + offending_item_summary = ( + f"{offending_item_summary[: max_sub_error_length - 3]}..." + ) return ( f"{message_prefix}: {output} '{path}', got {offending_item_summary}" f"{message_suffix}" diff --git a/tests/test_config.py b/tests/test_config.py index 27d28405989c..7345a3d03d28 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -20,6 +20,7 @@ from homeassistant.const import CONF_PACKAGES, __version__ from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import ConfigValidationError, HomeAssistantError from homeassistant.helpers import check_config, config_validation as cv +from homeassistant.helpers.redact import REDACTED from homeassistant.helpers.typing import ConfigType from homeassistant.loader import Integration, async_get_integration from homeassistant.util.yaml import SECRET_YAML, load_yaml_dict @@ -1532,6 +1533,33 @@ async def test_stringify_invalid_suggests_close_keys( ) +@pytest.mark.parametrize( + ("key", "expected_value"), + [ + pytest.param(probatio.Required("password"), "'hunter2'", id="plain"), + pytest.param( + probatio.Required(probatio.Secret("password")), REDACTED, id="secret" + ), + ], +) +async def test_stringify_invalid_redacts_a_secret( + hass: HomeAssistant, key: probatio.Marker, expected_value: str +) -> None: + """Test a key marked secret reports the reason without its value.""" + schema = probatio.Schema({key: probatio.All(str, probatio.Length(min=12))}) + config = {"password": "hunter2"} + + with pytest.raises(probatio.MultipleInvalid) as exc_info: + schema(config) + + assert config_util.stringify_invalid( + hass, exc_info.value.errors[0], "demo", config, None, 500 + ) == ( + "Invalid config for 'demo': length of value must be at least 12 for " + f"dictionary value 'password', got {expected_value}" + ) + + @pytest.mark.parametrize( "config_dir", ["packages", "packages_include_dir_named"],