fix(usage-bar): use Object.hasOwn instead of in operator to avoid prototype chain pollution (#98503)

The in operator traverses the prototype chain, causing keys like toString,
constructor, valueOf, and __proto__ to incorrectly match Object.prototype
inherited properties in alias table lookups and map segment case lookups.
Replace with Object.hasOwn (ES2022) which only checks own properties.

Fixes #98466

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
chenyangjun-xy
2026-07-01 18:37:45 +08:00
committed by GitHub
parent 1a160cec09
commit 73e6a626b8
2 changed files with 26 additions and 3 deletions
@@ -65,6 +65,17 @@ describe("usage-bar verbs", () => {
expect(render([{ text: "{m|alias:models}" }], { m: "some-new-model" })).toBe("some-new-model");
});
it("alias — prototype keys (toString, constructor) do not match inherited properties", () => {
// When a model is named "toString" or "constructor", the `in` operator
// would match Object.prototype inherited properties and return
// Object.prototype.toString (a function) instead of the raw key.
// After the fix (Object.hasOwn), these should echo through unchanged.
expect(render([{ text: "{m|alias:models}" }], { m: "toString" })).toBe("toString");
expect(render([{ text: "{m|alias:models}" }], { m: "constructor" })).toBe("constructor");
expect(render([{ text: "{m|alias:models}" }], { m: "valueOf" })).toBe("valueOf");
expect(render([{ text: "{m|alias:models}" }], { m: "__proto__" })).toBe("__proto__");
});
it("fallback when path is missing/empty", () => {
expect(render([{ text: "{identity.emoji|🤖} hi" }], {})).toBe("🤖 hi");
expect(render([{ text: "{identity.emoji|🤖} hi" }], { identity: { emoji: "🩺" } })).toBe(
@@ -87,6 +98,18 @@ describe("usage-bar segment forms", () => {
expect(render(seg, { state: {} })).toBe("");
});
it("map — prototype keys (toString, constructor) do not match inherited properties", () => {
// When the map key is "toString" or "constructor", the `in` operator
// would incorrectly match Object.prototype inherited properties and
// return undefined (Object.prototype.toString is a function, not a
// string case value) instead of falling through to _default.
const seg = [
{ map: "state.mode", cases: { toString: "should-not-match", _default: "fallback" } },
];
expect(render(seg, { state: { mode: "toString" } })).toBe("should-not-match");
expect(render(seg, { state: { mode: "constructor" } })).toBe("fallback");
});
it("each with item_scales picks a scale per window by position", () => {
const seg = [
{
+3 -3
View File
@@ -130,11 +130,11 @@ function applyVerb(name: string, args: string[], value: unknown, vocab: Vocab):
const table =
args[0] && isObject(aliases[args[0]]) ? (aliases[args[0]] as Record<string, unknown>) : {};
const key = String(value);
if (key in table) {
if (Object.hasOwn(table, key)) {
return table[key];
}
const lower = key.toLowerCase();
return lower in table ? table[lower] : value;
return Object.hasOwn(table, lower) ? table[lower] : value;
}
case "meter": {
const width = args[0] ? Number.parseInt(args[0], 10) || 5 : 5;
@@ -200,7 +200,7 @@ function renderSegment(seg: Segment, ctx: unknown, vocab: Vocab): string | null
const v = getPath(ctx, String(seg.map));
const key = typeof v === "boolean" ? String(v) : String(v);
const cases = isObject(seg.cases) ? seg.cases : {};
const hit = key in cases ? cases[key] : cases["_default"];
const hit = Object.hasOwn(cases, key) ? cases[key] : cases["_default"];
return typeof hit === "string" ? hit : null;
}
if ("each" in seg) {