[core] Sensitive redaction via yaml_util representer (#16690)

This commit is contained in:
J. Nick Koston
2026-05-27 09:20:50 -05:00
committed by GitHub
parent 3cc875c40b
commit 21e548f1d7
7 changed files with 306 additions and 10 deletions
+55
View File
@@ -15,6 +15,7 @@ from esphome.yaml_util import (
DiscoveredYamlFiles,
ESPHomeDataBase,
ESPLiteralValue,
SensitiveStr,
discover_user_yaml_files,
force_load_include_files,
format_path,
@@ -1340,3 +1341,57 @@ def test_frontmatter_included_file_stored(tmp_path: Path) -> None:
assert main.resolve() not in core.CORE.frontmatter
# Included file's frontmatter is captured
assert core.CORE.frontmatter[inc.resolve()]["child_meta"] == "hello"
def test_sensitive_str__is_a_str_subclass() -> None:
value = SensitiveStr("hunter2")
assert isinstance(value, str)
assert value == "hunter2"
def test_dump__redacts_sensitive_str_by_default() -> None:
out = yaml_util.dump({"password": SensitiveStr("hunter2")})
assert "\\033[8mhunter2\\033[28m" in out
assert "hunter2" not in out.replace(
"\\033[8mhunter2\\033[28m", ""
) # the raw value is only present inside the wrap
def test_dump__show_secrets_emits_sensitive_str_raw() -> None:
out = yaml_util.dump({"password": SensitiveStr("hunter2")}, show_secrets=True)
assert "hunter2" in out
assert "\\033[8m" not in out
assert "\\033[28m" not in out
def test_dump__plain_str_is_not_redacted() -> None:
out = yaml_util.dump({"hostname": "myserver"})
assert "myserver" in out
assert "\\033[8m" not in out
def test_dump__secret_reference_wins_over_redaction() -> None:
# If the value also has an entry in _SECRET_VALUES (i.e., it was loaded
# via !secret), the dump should render it as !secret <name>, not as a
# redacted scalar. SensitiveStr layered on top must not change that.
value = SensitiveStr("hunter2")
yaml_util._SECRET_VALUES[str(value)] = "my_secret_name"
try:
out = yaml_util.dump({"password": value})
assert "!secret" in out
assert "my_secret_name" in out
assert "\\033[8m" not in out
finally:
yaml_util._SECRET_VALUES.clear()
def test_dump__redaction_flag_does_not_leak_between_calls() -> None:
# Per-call _Dumper subclass means show_secrets in one call doesn't
# affect another. Run them in both orders to catch any leakage.
redacted = yaml_util.dump({"password": SensitiveStr("hunter2")})
raw = yaml_util.dump({"password": SensitiveStr("hunter2")}, show_secrets=True)
redacted_again = yaml_util.dump({"password": SensitiveStr("hunter2")})
assert "\\033[8m" in redacted
assert "\\033[8m" not in raw
assert "\\033[8m" in redacted_again