openlang

A typed merge helper still walks into `__proto__`

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?

后续讨论 2

顺着彼此的依据读下去
  1. 固定链接

    Skipping names turns into a list that's never finished. Tried it. Skip __proto__ and a file holding {"constructor": {"prototype": {"isAdmin": true}}} still lands on Object.prototype, because target.constructor on a plain object is Object and its prototype is the shared one. Now the list has three names and I don't trust it to stop there.

    From the screen side the whole thing is odd anyway. The import dialog shows two settings, theme and font size. User picks a file, the dialog says "Imported", and it only ever needed two keys. Read those two out of the parsed object, check each, drop the rest. Nothing walks keys the screen doesn't know about.

  2. 固定链接

    Did that for theme and font size, a decoder that reads the two known keys and ignores everything else. One part of the file doesn't fit that shape, though. shortcuts maps names the user chose to key combinations, so there's no list of keys to read. That part now goes into a Map.

    const shortcuts = new Map<string, string>(
      Object.entries(raw.shortcuts).filter(
        (e): e is [string, string] => typeof e[1] === "string",
      ),
    );

    A shortcut named __proto__ is just an entry, shortcuts.get("__proto__") returns its value, and no prototype is involved. The merge helper is gone from the import path.

同一个问题的其他角度

  1. Solidity reverts on overflow, Rust release builds wrap

    Since Solidity 0.8, arithmetic reverts on overflow and underflow. Turning that off takes an explicit unchecked { ... } block.…

    回复 4最近活动 solidityrust
  2. Testing a Go channel handoff without sleep

    Most handoff tests I run into end with a sleep and a hopeful assertion. This one can do without. A request carries an identif…

    回复 23最近活动 go
  3. partial def is the honest answer for Collatz

    Lean has to know a recursive def terminates before it accepts it as a definition. Structural recursion is found automatically…

    回复 10最近活动 lean-4