fix(net): stabilize shared wire graph audits

This commit is contained in:
2026-09-05 04:52:02 +08:00
parent 33904e0e58
commit e47d9d3007
2 changed files with 110 additions and 2 deletions
@@ -64,8 +64,11 @@ function propertyPath(parent: string, key: string | symbol): string {
}
function assertNoInheritedToJson(prototype: object | null, path: string): void {
const visited = new Set<object>();
let current = prototype;
while (current !== null) {
if (visited.has(current)) invalidEnvelope(path, 'prototype cycle is not allowed');
visited.add(current);
if (Object.getOwnPropertyDescriptor(current, 'toJSON')) {
invalidEnvelope(path, 'inherited toJSON is not allowed');
}
@@ -165,6 +168,7 @@ function snapshotJsonValue(
path: string,
ancestors: Set<object>,
audits: CapturedNode[],
snapshots: Map<object, unknown>,
): unknown {
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
if (typeof value === 'number') {
@@ -178,6 +182,7 @@ function snapshotJsonValue(
const objectValue = value as object;
if (ancestors.has(objectValue)) invalidEnvelope(path, 'cycle is not protocol JSON data');
if (snapshots.has(objectValue)) return snapshots.get(objectValue);
ancestors.add(objectValue);
try {
const captured = captureNode(objectValue, path, audits);
@@ -192,6 +197,7 @@ function snapshotJsonValue(
const allowedKeys = new Set<string>(['length']);
const snapshot: unknown[] = [];
Object.setPrototypeOf(snapshot, null);
snapshots.set(objectValue, snapshot);
for (let index = 0; index < (length as number); index++) {
const key = String(index);
const itemPath = `${path}[${index}]`;
@@ -204,6 +210,7 @@ function snapshotJsonValue(
itemPath,
ancestors,
audits,
snapshots,
);
Object.defineProperty(snapshot, key, {
configurable: true,
@@ -225,6 +232,7 @@ function snapshotJsonValue(
invalidEnvelope(path, 'expected plain object or null-prototype object');
}
const snapshot = Object.create(null) as Record<string, unknown>;
snapshots.set(objectValue, snapshot);
for (const { key } of captured.properties) {
if (typeof key === 'symbol') invalidEnvelope(path, 'symbol key is not allowed');
const nestedPath = childPath(path, key);
@@ -233,6 +241,7 @@ function snapshotJsonValue(
nestedPath,
ancestors,
audits,
snapshots,
);
Object.defineProperty(snapshot, key, {
configurable: true,
@@ -252,6 +261,7 @@ function serializeOutbound(envelope: OutboundEnvelope): string {
return invalidEnvelope('$', 'expected object');
}
const audits: CapturedNode[] = [];
const snapshots = new Map<object, unknown>();
const captured = captureNode(envelope, '$', audits);
if (captured.prototype !== Object.prototype && captured.prototype !== null) {
return invalidEnvelope('$', 'expected plain object or null-prototype object');
@@ -272,13 +282,21 @@ function serializeOutbound(envelope: OutboundEnvelope): string {
if (typeof rpc !== 'string' || rpc.length === 0) {
invalidEnvelope('$.rpc', 'expected non-empty string');
}
const snapshot = Object.create(null) as Record<string, unknown>;
snapshots.set(envelope, snapshot);
const ancestors = new Set<object>([envelope]);
let dataSnapshot: unknown;
try {
dataSnapshot = snapshotJsonValue(data, '$.data', ancestors, audits, snapshots);
} finally {
ancestors.delete(envelope);
}
const snapshotValues = new Map<string, unknown>([
['app', app],
['route', route],
['rpc', rpc],
['data', snapshotJsonValue(data, '$.data', new Set<object>(), audits)],
['data', dataSnapshot],
]);
const snapshot = Object.create(null) as Record<string, unknown>;
for (const { key } of captured.properties) {
Object.defineProperty(snapshot, key as string, {
configurable: true,