Skip to content

fix: keep own __proto__ keys in eval JSON results - #53

Merged
chhoumann merged 2 commits into
mainfrom
fix/44-own-proto-keys
Oct 24, 2026
Merged

chhoumann merged 2 commits into
mainfrom
fix/44-own-proto-keys

Conversation

@chhoumann

Copy link
Copy Markdown
Owner

Fixes #44.

Problem

An own __proto__ key in an eval result did not survive the trip to the caller.

  • client.dev.evalJson(...) returned {"savedFeeds":{}}, so the key was silently dropped.
  • client.dev.evalJsonAsync(...) timed out with reason still-pending. The operation had already finished in Obsidian.

Root cause

The in-app serializer copied entries into a {} literal. next["__proto__"] = value changes that object's prototype and does not create a key. On the sync path, JSON.stringify then skipped the missing key. On the async path, the swapped object was stored in the registry. Every poll read reserialized it, hit the non-plain-object check, and threw. The poll loop treated each of those throws as transient until the deadline expired.

Fix

  • Serializer records are built with Object.create(null), so __proto__ is an ordinary own key.
  • The client decoder already uses JSON.parse and Object.fromEntries. Both define the key as own data on an object whose prototype is Object.prototype, so a decoded result cannot pollute any prototype. The tests assert this.
  • A poll read that throws inside Obsidian (DevEvalError) is deterministic, so it is now rethrown at once instead of being reported as still-pending.

Verification

Tested against a real Obsidian 1.13.7 instance started with obsidian-e2e start, using a consumer project that installs the packed tarball:

before:
evalJson:       {"savedFeeds":{}} ownProtoKey=false polluted=false
evalJsonAsync threw: DevEvalAsyncError The evalJsonAsync operation is still pending in Obsidian after 2000ms; ...
after:
evalJson:       {"savedFeeds":{"__proto__":{"title":"__proto__"}}} ownProtoKey=true polluted=false
evalJsonAsync:  {"savedFeeds":{"__proto__":{"title":"__proto__"}}} ownProtoKey=true polluted=false

The regression tests are committed before the fix and fail without it. They cover the sync path, the async path, and a poll read that fails in-app.

The in-app serializer copied entries into a {} literal, so an own
__proto__ key swapped that object's prototype instead of becoming a key.
evalJson dropped the key. evalJsonAsync stored the swapped object, every
poll read then failed to reserialize it, and the loop retried those
failures until it reported a finished operation as still-pending.

Build serializer records with a null prototype. The client decodes them
with Object.fromEntries, which defines the key as own data on a plain
object, so nothing can pollute a prototype. A poll that throws in-app is
deterministic, so it now surfaces at once instead of being retried.

Fixes #44
@chhoumann
chhoumann marked this pull request as ready for review October 24, 2026 14:29
@chhoumann
chhoumann merged commit e1e08a6 into main Oct 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: preserve own __proto__ keys in eval JSON serialization

1 participant