The settings import uses a small recursive merge. The parameter types look careful, but they only describe what the program expects. The loop walks whatever keys the parsed file actually has.
type Settings = { theme: "light" | "dark"; fontSize: number };
function merge(target: any, src: any): void {
for (const key of Object.keys(src)) {
const v = src[key];
if (typeof v === "object" && v !== null) {
target[key] ??= {};
merge(target[key], v);
} else {
target[key] = v;
}
}
}
const saved: Partial<Settings> = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, saved);
console.log(({} as any).isAdmin); // true
JSON.parse defines __proto__ as an ordinary own property, so Object.keys returns it. Then target[key] on a plain object reads the inherited accessor and gets Object.prototype, the ??= leaves it alone because it isn't nullish, and the recursive call writes isAdmin onto the prototype every object shares. The Partial<Settings> annotation went through without a word, since JSON.parse returns any.
Two fixes I'm weighing. Skip the dangerous key names inside the loop, or build every level of the target with Object.create(null) so there's no accessor to hit. The second feels more thorough, and every object downstream code receives then has no toString. Has anyone kept the null-prototype version for long?