fix: decode resumable control-plane responses against hostile objects
TR-RR-08. Object.keys sees only enumerable own string keys, so a symbol or non-enumerable extra field passed the exactness check unseen and the property reads that followed invoked whatever accessor the sender installed — escaping the Result contract as a native rejection out of a public method. Key exactness is now checked against own property descriptors inside a catch, and each decode runs within the adapter's failure boundary so a proxy trap becomes a typed CORRUPT_DATA result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fb5b449031
commit
69cb7e35ca
@@ -104,7 +104,7 @@ export function createResumableUploadHttpControlPlane(
|
||||
"UPLOAD_SESSION",
|
||||
);
|
||||
if (!response.ok) return response;
|
||||
const session = decodeSession(response.value);
|
||||
const session = decodeSafely(decodeSession, response.value);
|
||||
return session
|
||||
? browserDataSuccess(session)
|
||||
: browserDataFailure(
|
||||
@@ -140,7 +140,7 @@ export function createResumableUploadHttpControlPlane(
|
||||
"UPLOAD_RECONCILE",
|
||||
);
|
||||
if (!response.ok) return response;
|
||||
const status = decodeStatus(response.value);
|
||||
const status = decodeSafely(decodeStatus, response.value);
|
||||
return status
|
||||
? browserDataSuccess(status)
|
||||
: browserDataFailure(
|
||||
@@ -270,7 +270,7 @@ export function createResumableUploadHttpControlPlane(
|
||||
"UPLOAD_COMPLETE",
|
||||
);
|
||||
if (!response.ok) return response;
|
||||
const completed = decodeCompletion(response.value);
|
||||
const completed = decodeSafely(decodeCompletion, response.value);
|
||||
return completed
|
||||
? browserDataSuccess(completed)
|
||||
: browserDataFailure(
|
||||
@@ -579,6 +579,15 @@ function snapshotReceipt(value: UploadPartReceipt): UploadPartReceipt {
|
||||
return Object.freeze({ ...value });
|
||||
}
|
||||
|
||||
/**
|
||||
* TR-RR-08. `Object.keys` sees only enumerable own string keys, so a symbol or
|
||||
* non-enumerable extra field passed unseen and a later property read invoked
|
||||
* whatever accessor the sender installed — escaping the Result contract as a
|
||||
* rejection of the public method.
|
||||
*
|
||||
* Every key is checked against its own property descriptor, and the whole probe
|
||||
* runs inside a catch so a proxy trap is a decode failure, not an exception.
|
||||
*/
|
||||
function exactKeys(
|
||||
value: unknown,
|
||||
keys: readonly string[],
|
||||
@@ -586,12 +595,39 @@ function exactKeys(
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
const actual = Object.keys(value).sort();
|
||||
const expected = [...keys].sort();
|
||||
return (
|
||||
actual.length === expected.length &&
|
||||
actual.every((key, index) => key === expected[index])
|
||||
);
|
||||
try {
|
||||
if (Object.getOwnPropertySymbols(value).length > 0) return false;
|
||||
const actual = Object.getOwnPropertyNames(value).sort();
|
||||
const expected = [...keys].sort();
|
||||
if (
|
||||
actual.length !== expected.length ||
|
||||
actual.some((key, index) => key !== expected[index])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return actual.every((key) => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
||||
return Boolean(descriptor && "value" in descriptor);
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TR-RR-08. Runs a decoder inside the adapter's failure boundary. A hostile
|
||||
* object that still throws from a trap becomes a typed `CORRUPT_DATA` result
|
||||
* rather than a native rejection out of a public method.
|
||||
*/
|
||||
function decodeSafely<Value>(
|
||||
decode: (value: unknown) => Value | null,
|
||||
value: unknown,
|
||||
): Value | null {
|
||||
try {
|
||||
return decode(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeIdempotencyKey(value: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user