Skip to content

Commit 6db56f2

Browse files
committed
Mask password values in object selector previews
The collapsed preview of an object selector list item rendered every field value verbatim, including text fields configured as a password. Add-on list options that hold a password (like the Mosquitto broker credentials) showed the secret in plain text in the read-only preview. Mask password-type text values in formatSelectorValue so the preview shows bullets instead of the secret. The underlying value is untouched, so editing and submitting still use the real value.
1 parent b2dda0f commit 6db56f2

3 files changed

Lines changed: 74 additions & 1 deletion

File tree

gallery/src/pages/components/ha-selector.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,10 @@ const SCHEMAS: {
502502
},
503503
},
504504
},
505+
password: {
506+
label: "Password",
507+
selector: { text: { type: "password" } },
508+
},
505509
},
506510
},
507511
},

src/data/selector/format_selector_value.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,15 @@ export const formatSelectorValue = (
1818
}
1919

2020
if ("text" in selector) {
21-
const { prefix, suffix } = selector.text || {};
21+
const { prefix, suffix, type } = selector.text || {};
2222

2323
const texts = ensureArray(value);
24+
25+
// Never reveal secret values in a read-only preview.
26+
if (type === "password") {
27+
return texts.map(() => "••••••••").join(", ");
28+
}
29+
2430
return texts
2531
.map((text) => `${prefix || ""}${text}${suffix || ""}`)
2632
.join(", ");
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, it, expect } from "vitest";
2+
import { formatSelectorValue } from "../../src/data/selector/format_selector_value";
3+
import type { HomeAssistant } from "../../src/types";
4+
5+
// formatSelectorValue only touches hass for floor/area/entity/device
6+
// selectors, none of which these tests exercise.
7+
const hass = {} as HomeAssistant;
8+
9+
describe("formatSelectorValue", () => {
10+
it("returns an empty string for nullish values", () => {
11+
expect(formatSelectorValue(hass, null)).toBe("");
12+
expect(formatSelectorValue(hass, undefined)).toBe("");
13+
});
14+
15+
it("renders a plain text value", () => {
16+
expect(formatSelectorValue(hass, "hello", { text: { type: "text" } })).toBe(
17+
"hello"
18+
);
19+
});
20+
21+
it("applies prefix and suffix for text selectors", () => {
22+
expect(
23+
formatSelectorValue(hass, "5", {
24+
text: { type: "text", prefix: "$", suffix: " each" },
25+
})
26+
).toBe("$5 each");
27+
});
28+
29+
it("masks a password text value instead of revealing it", () => {
30+
const result = formatSelectorValue(hass, "hunter2", {
31+
text: { type: "password" },
32+
});
33+
expect(result).not.toContain("hunter2");
34+
expect(result).toBe("••••••••");
35+
});
36+
37+
it("masks every value of a multiple password selector", () => {
38+
const result = formatSelectorValue(hass, ["one", "two"], {
39+
text: { type: "password" },
40+
});
41+
expect(result).not.toContain("one");
42+
expect(result).not.toContain("two");
43+
expect(result).toBe("••••••••, ••••••••");
44+
});
45+
46+
it("masks a nested password field in an object selector preview", () => {
47+
const result = formatSelectorValue(
48+
hass,
49+
{ username: "admin", password: "hunter2" },
50+
{
51+
object: {
52+
fields: {
53+
username: { selector: { text: { type: "text" } } },
54+
password: { selector: { text: { type: "password" } } },
55+
},
56+
},
57+
}
58+
);
59+
expect(result).toContain("admin");
60+
expect(result).not.toContain("hunter2");
61+
expect(result).toContain("••••••••");
62+
});
63+
});

0 commit comments

Comments
 (0)