Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/eval-json-proto-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"obsidian-e2e": patch
---

`evalJson` and `evalJsonAsync` now keep an own `__proto__` key in a result as a plain data key. `evalJson` used to drop it, and `evalJsonAsync` timed out with reason `still-pending` although the operation had finished. The decoded result still has `Object.prototype` as its prototype, so the key cannot pollute any prototype. A poll read that fails inside Obsidian now throws its `DevEvalError` at once.
5 changes: 4 additions & 1 deletion src/dev/eval-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ const EVAL_JSON_SERIALIZER = [
"if(Array.isArray(value)){return value.map((item,index)=>__obsidianE2ESerialize(item,`${path}[${index}]`));}",
"const prototype=Object.getPrototypeOf(value);",
"if(prototype!==Object.prototype&&prototype!==null){throw new Error(`Cannot serialize non-plain object at ${path}.`);}",
"const next={};",
// A null prototype keeps an own `__proto__` key as data instead of a prototype swap.
"const next=Object.create(null);",
"for(const [key,entry] of Object.entries(value)){next[key]=__obsidianE2ESerialize(entry,`${path}.${key}`);}",
"return next;",
"};",
Expand Down Expand Up @@ -240,6 +241,8 @@ export async function runEvalJsonAsync<T>(
directCommandOptions(ASYNC_EVAL_COMMAND_TIMEOUT_MS),
);
} catch (error) {
// The read ran in-app and threw, so repeating it cannot succeed.
if (error instanceof DevEvalError) throw error;
lastError = error;
await pause();
continue;
Expand Down
31 changes: 31 additions & 0 deletions tests/dev/eval-json-async.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,37 @@ describe("runEvalJsonAsync", () => {
await expect(runEvalJsonAsync(dev, "await Promise.resolve('polled')")).resolves.toBe("polled");
});

it("resolves an own __proto__ key as data without touching any prototype", async () => {
const { dev } = createInAppEmulator();

const value = await runEvalJsonAsync<{ savedFeeds: Record<string, unknown> }>(
dev,
'JSON.parse(\'{"savedFeeds":{"__proto__":{"title":"__proto__"}}}\')',
{ timeoutMs: 2_000 },
);

expect(Object.getOwnPropertyDescriptor(value.savedFeeds, "__proto__")?.value).toEqual({
title: "__proto__",
});
expect(Object.getPrototypeOf(value.savedFeeds)).toBe(Object.prototype);
expect(({} as Record<string, unknown>).title).toBeUndefined();
});

it("rethrows a poll that fails in-app instead of reporting the operation as pending", async () => {
const { dev } = createInAppEmulator(() => {
// A completed entry the poll read cannot serialize.
for (const entry of Object.values(registryEntries())) {
if (entry.state === "done")
Object.assign(entry, { envelope: { ok: true, value: new Map() } });
}
return undefined;
});

await expect(
runEvalJsonAsync(dev, "await Promise.resolve('done')", { timeoutMs: 2_000 }),
).rejects.toThrowError(DevEvalError);
});

it("fails with reason 'still-pending' when the promise never settles", async () => {
const { dev } = createInAppEmulator();

Expand Down
12 changes: 12 additions & 0 deletions tests/dev/eval-json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ describe("buildEvalJsonCode executed", () => {
expect(thrown).toBeInstanceOf(DevEvalError);
expect((thrown as DevEvalError).message).toBe("Failed to evaluate Obsidian code: nope");
});

it("keeps an own __proto__ key as data without touching any prototype", () => {
const value = evalGenerated<{ savedFeeds: Record<string, unknown> }>(
'JSON.parse(\'{"savedFeeds":{"__proto__":{"title":"__proto__"}}}\')',
);

expect(Object.getOwnPropertyDescriptor(value.savedFeeds, "__proto__")?.value).toEqual({
title: "__proto__",
});
expect(Object.getPrototypeOf(value.savedFeeds)).toBe(Object.prototype);
expect(({} as Record<string, unknown>).title).toBeUndefined();
});
});

// Regression for https://github.com/chhoumann/obsidian-e2e/issues/18 on the
Expand Down
Loading