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.
pull/18629/head
Luan Taraschi 1 month ago
parent 26786e9298
commit 82fc329384

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: keep an own `__proto__` key in `$state.snapshot`

@ -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;

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<div>["__proto__","b"]</div><div>true</div><div></div>`
});

@ -0,0 +1,11 @@
<script>
// An own `__proto__` key comes from JSON, which is where state hydrated
// from a response, from storage, or from a query string comes from.
let state = $state(JSON.parse('{"__proto__":{"admin":true},"b":2}'));
const snapshot = $state.snapshot(state);
</script>
<div>{JSON.stringify(Object.keys(snapshot))}</div>
<div>{Object.getPrototypeOf(snapshot) === Object.prototype}</div>
<div>{snapshot.admin}</div>
Loading…
Cancel
Save