Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions yaml/_loader_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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);
}

Expand Down
23 changes: 23 additions & 0 deletions yaml/parse_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
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(
Expand Down
Loading