fix(marshal): Detect unexpected nonenumerable properties on tagged records and remotables
Fixes
MODIFY
packages/marshal/src/helpers/remotable.js
packages/marshal/src/helpers/remotable.js
@@ -132,15 +132,21 @@ const checkRemotableProtoOf = (original, check) => {
assert.fail(X`unrecognized typeof ${original}`);
}
+ // Typecasts needed due to https://github.com/microsoft/TypeScript/issues/1863
+ const passStyleKey = /** @type {unknown} */ (PASS_STYLE);
+ const tagKey = /** @type {unknown} */ (Symbol.toStringTag);
const {
- [PASS_STYLE]: _passStyle,
- [Symbol.toStringTag]: iface,
- ...rest
- } = proto;
+ // checkTagRecord already verified PASS_STYLE and Symbol.toStringTag own data properties.
+ [/** @type {string} */ (passStyleKey)]: _passStyleDesc,
+ [/** @type {string} */ (tagKey)]: { value: iface },
+ ...restDescs
+ } = getOwnPropertyDescriptors(proto);
return (
- (ownKeys(rest).length === 0 ||
- reject(X`Unexpected properties on Remotable Proto ${ownKeys(rest)}`)) &&
+ (ownKeys(restDescs).length === 0 ||
+ reject(
+ X`Unexpected properties on Remotable Proto ${ownKeys(restDescs)}`,
+ )) &&
checkIface(iface, check)
);
};

MODIFY
packages/marshal/src/helpers/tagged.js
packages/marshal/src/helpers/tagged.js
@@ -11,7 +11,11 @@ import {
const { details: X } = assert;
const { ownKeys } = Reflect;
-const { getPrototypeOf, prototype: objectPrototype } = Object;
+const {
+ getOwnPropertyDescriptors,
+ getPrototypeOf,
+ prototype: objectPrototype,
+} = Object;
/**
*
@@ -27,18 +31,24 @@ export const TaggedHelper = harden({
getPrototypeOf(candidate) === objectPrototype ||
assert.fail(X`Unexpected prototype for: ${candidate}`);
+ // Typecasts needed due to https://github.com/microsoft/TypeScript/issues/1863
+ const passStyleKey = /** @type {unknown} */ (PASS_STYLE);
+ const tagKey = /** @type {unknown} */ (Symbol.toStringTag);
const {
- [PASS_STYLE]: _passStyle, // checkTagRecord already checked
- [Symbol.toStringTag]: _label, // checkTagRecord already checked
- payload: _payload, // value checked by recursive walk at the end
- ...rest
- } = candidate;
- (ownKeys(rest).length === 0) ||
- assert.fail(X`Unexpected properties on Remotable Proto ${ownKeys(rest)}`);
+ // checkTagRecord already verified PASS_STYLE and Symbol.toStringTag own data properties.
+ [/** @type {string} */ (passStyleKey)]: _passStyleDesc,
+ [/** @type {string} */ (tagKey)]: _labelDesc,
+ payload: _payloadDesc, // value checked by recursive walk at the end
+ ...restDescs
+ } = getOwnPropertyDescriptors(candidate);
+ (ownKeys(restDescs).length === 0) ||
+ assert.fail(
+ X`Unexpected properties on tagged record ${ownKeys(restDescs)}`,
+ );
checkNormalProperty(candidate, 'payload', 'string', true, assertChecker);
// Recursively validate that each member is passable.
- !!passStyleOfRecur(candidate.payload);
+ passStyleOfRecur(candidate.payload);
},
});

MODIFY
packages/marshal/test/test-passStyleOf.js
packages/marshal/test/test-passStyleOf.js
@@ -6,6 +6,7 @@ import { makeTagged } from '../src/makeTagged.js';
import { PASS_STYLE } from '../src/helpers/passStyle-helpers.js';
const { getPrototypeOf } = Object;
+const { ownKeys } = Reflect;
test('passStyleOf basic success cases', t => {
// Test in same order as `passStyleOf` for easier maintenance.
@@ -67,9 +68,9 @@ test('some passStyleOf rejections', t => {
});
/**
- * For testing purposes, makes a TagRecond-like object with
+ * For testing purposes, makes a TagRecord-like object with
* non-enumerable PASS_STYLE and Symbol.toStringTag properties.
- * A valid Remotable must inherit from a valid TagRecord
+ * A valid Remotable must inherit from a valid TagRecord.
*
* @param {string} [tag]
* @param {object|null} [proto]
@@ -81,6 +82,66 @@ const makeTagishRecord = (tag = 'Remotable', proto = undefined) => {
});
};
+test('passStyleOf testing tagged records', t => {
+ const makeTagRecordVariant = (payload, proto = undefined) => {
+ const record = Object.create(
+ proto === undefined ? Object.prototype : proto,
+ {
+ [PASS_STYLE]: { value: 'tagged' },
+ [Symbol.toStringTag]: { value: 'tagged' },
+ },
+ );
+ record.payload = payload;
+ return record;
+ };
+ t.is(passStyleOf(harden(makeTagRecordVariant())), 'tagged');
+ t.is(passStyleOf(harden(makeTagRecordVariant({ passable: true }))), 'tagged');
+
+ for (const proto of [null, {}]) {
+ const tagRecordBadProto = makeTagRecordVariant(undefined, proto);
+ t.throws(() => passStyleOf(harden(tagRecordBadProto)), {
+ message: /Unexpected prototype/,
+ });
+ }
+
+ const tagRecordExtra = makeTagRecordVariant();
+ Object.defineProperty(tagRecordExtra, 'extra', {
+ value: 'unexpected own property',
+ });
+ t.throws(() => passStyleOf(harden(tagRecordExtra)), {
+ message: 'Unexpected properties on tagged record ["extra"]',
+ });
+
+ const tagRecordBadPayloads = [
+ { label: 'absent', message: '"payload" property expected: "[tagged]"' },
+ {
+ label: 'non-enumerable',
+ value: 0,
+ enumerable: false,
+ message: '"payload" must be an enumerable property: "[tagged]"',
+ },
+ {
+ label: 'non-passable',
+ value: { [PASS_STYLE]: 0 },
+ message: '0 must be a string',
+ },
+ ];
+ for (const testCase of tagRecordBadPayloads) {
+ const { label, message, ...desc } = testCase;
+ const tagRecordBadPayload = makeTagRecordVariant();
+ if (ownKeys(desc).length === 0) {
+ delete tagRecordBadPayload.payload;
+ } else {
+ Object.defineProperty(tagRecordBadPayload, 'payload', desc);
+ }
+ t.throws(
+ () => passStyleOf(harden(tagRecordBadPayload)),
+ { message },
+ `tagged record with payload ${label} must be rejected`,
+ );
+ }
+});
+
test('passStyleOf testing remotables', t => {
t.is(passStyleOf(Far('foo', {})), 'remotable');
t.is(passStyleOf(Far('foo', () => 'far function')), 'remotable');
@@ -218,6 +279,16 @@ test('passStyleOf testing remotables', t => {
'cannot serialize Remotables with non-methods like "Symbol(passStyle)" in "[Alleged: manually constructed]"',
});
+ const farObjProtoWithExtra = makeTagishRecord(
+ 'Alleged: manually constructed',
+ null,
+ );
+ Object.defineProperty(farObjProtoWithExtra, 'extra', { value: () => {} });
+ const badFarObjExtraProtoProp = harden({ __proto__: farObjProtoWithExtra });
+ t.throws(() => passStyleOf(badFarObjExtraProtoProp), {
+ message: 'Unexpected properties on Remotable Proto ["extra"]',
+ });
+
passStyleOf(harden({ __proto__: Object.prototype }), 'copyRecord');
const farObjC = harden({

fix(marshal): Detect unexpected nonenumerable properties on tagged records and remotables

Fixes #1316