From 82fc329384de798b530b9b7fc99bbc8ab5d93101 Mon Sep 17 00:00:00 2001 From: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com> Date: Sat, 8 Aug 2026 08:40:04 -0300 Subject: [PATCH] fix: keep an own __proto__ key in $state.snapshot The plain-object branch of clone copies keys onto a fresh `{}` with `copy[key] = ...`. For the key `__proto__` that runs the setter inherited from Object.prototype instead of creating a property, so the key is dropped from the snapshot and, when its value is an object, becomes the snapshot's prototype. The copy then answers for fields that were data: const state = $state(JSON.parse('{"__proto__":{"admin":true},"b":2}')) const snap = $state.snapshot(state) Object.keys(snap) // ['b'] snap.admin // true An own __proto__ key does not come from an object literal, it comes from JSON, which is where state hydrated from a response, from storage, or from a query string comes from. structuredClone, which this same function falls back to for everything it does not walk itself, keeps the key as an own property. Define the slot so the walked path answers the same way. --- .changeset/snapshot-own-proto-key.md | 5 +++++ packages/svelte/src/internal/shared/clone.js | 20 +++++++++++++++++-- .../state-snapshot-proto-key/_config.js | 5 +++++ .../state-snapshot-proto-key/main.svelte | 11 ++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 .changeset/snapshot-own-proto-key.md create mode 100644 packages/svelte/tests/runtime-runes/samples/state-snapshot-proto-key/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/state-snapshot-proto-key/main.svelte diff --git a/.changeset/snapshot-own-proto-key.md b/.changeset/snapshot-own-proto-key.md new file mode 100644 index 0000000000..9d99362c1e --- /dev/null +++ b/.changeset/snapshot-own-proto-key.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: keep an own `__proto__` key in `$state.snapshot` diff --git a/packages/svelte/src/internal/shared/clone.js b/packages/svelte/src/internal/shared/clone.js index 1a83adf8c5..1881a4fff2 100644 --- a/packages/svelte/src/internal/shared/clone.js +++ b/packages/svelte/src/internal/shared/clone.js @@ -1,7 +1,7 @@ /** @import { Snapshot } from './types' */ import { DEV } from 'esm-env'; import * as w from './warnings.js'; -import { get_prototype_of, is_array, object_prototype } from './utils.js'; +import { define_property, get_prototype_of, is_array, object_prototype } from './utils.js'; /** * In dev, we keep track of which properties could not be cloned. In prod @@ -90,7 +90,7 @@ function clone(value, cloned, path, paths, original = null, no_tojson = false) { } for (var key of Object.keys(value)) { - copy[key] = clone( + var cloned_value = clone( // @ts-expect-error value[key], cloned, @@ -99,6 +99,22 @@ function clone(value, cloned, path, paths, original = null, no_tojson = false) { null, no_tojson ); + + if (key === '__proto__') { + // Assigning `__proto__` runs the setter inherited from `Object.prototype` + // rather than creating a property, so the key would be dropped and an + // object value would become the copy's prototype, leaving the snapshot + // inheriting fields that were data. `structuredClone`, which this + // function falls back to below, keeps it as an own property. + define_property(copy, key, { + value: cloned_value, + writable: true, + enumerable: true, + configurable: true + }); + } else { + copy[key] = cloned_value; + } } return copy; diff --git a/packages/svelte/tests/runtime-runes/samples/state-snapshot-proto-key/_config.js b/packages/svelte/tests/runtime-runes/samples/state-snapshot-proto-key/_config.js new file mode 100644 index 0000000000..d589156c07 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-snapshot-proto-key/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `