Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 6cdb3c7

Browse files
authored
Add additional traps to magic proxy stubs (#710)
This change adds `getOwnPropertyDescriptor`, `ownKeys` and `getPrototypeOf` traps to magic proxy stubs. The first two allow proxy stubs to be `JSON.stringify`ed. The last ensures proxies aren't detected as plain objects.
1 parent e63211f commit 6cdb3c7

File tree

4 files changed

+96
-4
lines changed

4 files changed

+96
-4
lines changed

packages/miniflare/src/plugins/core/proxy/client.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,12 @@ class ProxyClientBridge {
185185
class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
186186
readonly #version: number;
187187
readonly #stringifiedTarget: string;
188-
readonly #known = new Map<string, unknown>();
188+
readonly #knownValues = new Map<string, unknown>();
189+
readonly #knownDescriptors = new Map<
190+
string,
191+
PropertyDescriptor | undefined
192+
>();
193+
#knownOwnKeys?: string[];
189194

190195
revivers: ReducersRevivers = {
191196
...revivers,
@@ -327,7 +332,7 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
327332
if (typeof key === "symbol" || key === "then") return undefined;
328333

329334
// See optimisation comments below for cases where this will be set
330-
const maybeKnown = this.#known.get(key);
335+
const maybeKnown = this.#knownValues.get(key);
331336
if (maybeKnown !== undefined) return maybeKnown;
332337

333338
// Always perform a synchronous GET, if this returns a `Promise`, we'll
@@ -361,7 +366,7 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
361366
// (e.g. accessing `R2ObjectBody#body` multiple times)
362367
result instanceof ReadableStream
363368
) {
364-
this.#known.set(key, result);
369+
this.#knownValues.set(key, result);
365370
}
366371
return result;
367372
}
@@ -371,6 +376,54 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
371376
return this.get(target, key, undefined) !== undefined;
372377
}
373378

379+
getOwnPropertyDescriptor(target: T, key: string | symbol) {
380+
if (typeof key === "symbol") return undefined;
381+
382+
// Optimisation: assume constant prototypes of proxied objects, descriptors
383+
// should never change after we've fetched them
384+
const maybeKnown = this.#knownDescriptors.get(key);
385+
if (maybeKnown !== undefined) return maybeKnown;
386+
387+
const syncRes = this.bridge.sync.fetch(this.bridge.url, {
388+
method: "POST",
389+
headers: {
390+
[CoreHeaders.OP]: ProxyOps.GET_OWN_DESCRIPTOR,
391+
[CoreHeaders.OP_KEY]: key,
392+
[CoreHeaders.OP_TARGET]: this.#stringifiedTarget,
393+
},
394+
});
395+
const result = this.#parseSyncResponse(
396+
syncRes,
397+
this.getOwnPropertyDescriptor
398+
) as PropertyDescriptor | undefined;
399+
400+
this.#knownDescriptors.set(key, result);
401+
return result;
402+
}
403+
404+
ownKeys(_target: T) {
405+
// Optimisation: assume constant prototypes of proxied objects, own keys
406+
// should never change after we've fetched them
407+
if (this.#knownOwnKeys !== undefined) return this.#knownOwnKeys;
408+
409+
const syncRes = this.bridge.sync.fetch(this.bridge.url, {
410+
method: "POST",
411+
headers: {
412+
[CoreHeaders.OP]: ProxyOps.GET_OWN_KEYS,
413+
[CoreHeaders.OP_TARGET]: this.#stringifiedTarget,
414+
},
415+
});
416+
const result = this.#parseSyncResponse(syncRes, this.ownKeys) as string[];
417+
418+
this.#knownOwnKeys = result;
419+
return result;
420+
}
421+
422+
getPrototypeOf(_target: T) {
423+
// Return a `null` prototype, so users know this isn't a plain object
424+
return null;
425+
}
426+
374427
#createFunction(key: string) {
375428
// Optimisation: if the function returns a `Promise`, we know it must be
376429
// async (assuming all async functions always return `Promise`s). When

packages/miniflare/src/workers/core/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export const CoreBindings = {
3030
export const ProxyOps = {
3131
// Get the target or a property of the target
3232
GET: "GET",
33+
// Get the descriptor for a property of the target
34+
GET_OWN_DESCRIPTOR: "GET_OWN_DESCRIPTOR",
35+
// Get the target's own property names
36+
GET_OWN_KEYS: "GET_OWN_KEYS",
3337
// Call a method on the target
3438
CALL: "CALL",
3539
// Remove the strong reference to the target on the "heap", allowing it to be

packages/miniflare/src/workers/core/proxy.worker.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export class ProxyServer implements DurableObject {
156156
const targetName = target.constructor.name;
157157

158158
let status = 200;
159-
let result;
159+
let result: unknown;
160160
let unbufferedRest: ReadableStream | undefined;
161161
if (opHeader === ProxyOps.GET) {
162162
// If no key header is specified, just return the target
@@ -168,6 +168,18 @@ export class ProxyServer implements DurableObject {
168168
headers: { [CoreHeaders.OP_RESULT_TYPE]: "Function" },
169169
});
170170
}
171+
} else if (opHeader === ProxyOps.GET_OWN_DESCRIPTOR) {
172+
if (keyHeader === null) return new Response(null, { status: 400 });
173+
const descriptor = Object.getOwnPropertyDescriptor(target, keyHeader);
174+
if (descriptor !== undefined) {
175+
result = <PropertyDescriptor>{
176+
configurable: descriptor.configurable,
177+
enumerable: descriptor.enumerable,
178+
writable: descriptor.writable,
179+
};
180+
}
181+
} else if (opHeader === ProxyOps.GET_OWN_KEYS) {
182+
result = Object.getOwnPropertyNames(target);
171183
} else if (opHeader === ProxyOps.CALL) {
172184
// We don't allow callable targets yet (could be useful to implement if
173185
// we ever need to proxy functions that return functions)

packages/miniflare/test/plugins/core/proxy/client.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,26 @@ test("ProxyClient: returns empty ReadableStream synchronously", async (t) => {
215215
assert(objectBody != null);
216216
t.is(await text(objectBody.body), ""); // Synchronous empty stream access
217217
});
218+
test("ProxyClient: can `JSON.stringify()` proxies", async (t) => {
219+
const mf = new Miniflare({ script: nullScript, r2Buckets: ["BUCKET"] });
220+
t.teardown(() => mf.dispose());
221+
222+
const bucket = await mf.getR2Bucket("BUCKET");
223+
const object = await bucket.put("key", "value");
224+
assert(object !== null);
225+
t.is(Object.getPrototypeOf(object), null);
226+
const plainObject = JSON.parse(JSON.stringify(object));
227+
t.deepEqual(plainObject, {
228+
checksums: {
229+
md5: "2063c1608d6e0baf80249c42e2be5804",
230+
},
231+
customMetadata: {},
232+
etag: "2063c1608d6e0baf80249c42e2be5804",
233+
httpEtag: '"2063c1608d6e0baf80249c42e2be5804"',
234+
httpMetadata: {},
235+
key: "key",
236+
size: 5,
237+
uploaded: object.uploaded.toISOString(),
238+
version: object.version,
239+
});
240+
});

0 commit comments

Comments
 (0)