diff --git a/yaml/_loader_state.ts b/yaml/_loader_state.ts index d58ecbf385f6..c7574807d89e 100644 --- a/yaml/_loader_state.ts +++ b/yaml/_loader_state.ts @@ -451,12 +451,21 @@ export class LoaderState { for (const [key, value] of Object.entries(source)) { if (Object.hasOwn(destination, key)) continue; - Object.defineProperty(destination, key, { - value, - writable: true, - enumerable: true, - configurable: true, - }); + // `Object.defineProperty` is significantly slower than direct + // assignment in V8. Direct assignment produces an identical descriptor + // (writable/enumerable/configurable) for ordinary keys; the only + // sensitive case is `__proto__`, where direct assignment would mutate + // the prototype chain instead of creating an own property. + if (key === "__proto__") { + Object.defineProperty(destination, key, { + value, + writable: true, + enumerable: true, + configurable: true, + }); + } else { + destination[key] = value; + } overridableKeys.add(key); } } @@ -523,12 +532,18 @@ export class LoaderState { this.#scanner.position = startPos || this.#scanner.position; throw this.#createError("Cannot store mapping pair: duplicated key"); } - Object.defineProperty(result, keyNode, { - value: valueNode, - writable: true, - enumerable: true, - configurable: true, - }); + // See `mergeMappings` above for why `Object.defineProperty` is kept + // only for the `__proto__` key. + if (keyNode === "__proto__") { + Object.defineProperty(result, keyNode, { + value: valueNode, + writable: true, + enumerable: true, + configurable: true, + }); + } else { + result[keyNode] = valueNode; + } overridableKeys.delete(keyNode); } diff --git a/yaml/parse_test.ts b/yaml/parse_test.ts index f875fc4843b6..cc3cd34a477b 100644 --- a/yaml/parse_test.ts +++ b/yaml/parse_test.ts @@ -1071,6 +1071,29 @@ Deno.test("parse() throws at reseverd characters '`' and '@'", () => { ); }); +Deno.test("parse() does not pollute prototype with `__proto__` key", () => { + // A YAML key of `__proto__` must produce an own property on the result, + // not mutate the result's prototype chain. + const result = parse("__proto__:\n polluted: true") as Record< + string, + unknown + >; + assert(Object.hasOwn(result, "__proto__")); + assertEquals( + (result as { __proto__: unknown }).__proto__, + { polluted: true }, + ); + // Same guarantee for the merge type (which goes through `mergeMappings`). + const merged = parse(`<<: + __proto__: + polluted: true +ok: 1`) as Record; + assert(Object.hasOwn(merged, "__proto__")); + assertEquals(merged.ok, 1); + // Sanity: an unrelated object's prototype is untouched. + assertEquals(({} as { polluted?: unknown }).polluted, undefined); +}); + Deno.test("parse() handles sequence", () => { assertEquals(parse("[]"), []); assertEquals(