fix(code-runtime): harden captured JSON boundary
This commit is contained in:
@@ -12,6 +12,7 @@ const intrinsicReflectApply = Reflect.apply as (
|
|||||||
const intrinsicArrayIsArray = Array.isArray
|
const intrinsicArrayIsArray = Array.isArray
|
||||||
const IntrinsicBuffer = Buffer
|
const IntrinsicBuffer = Buffer
|
||||||
const intrinsicBufferByteLength = Reflect.get(Buffer, 'byteLength') as IntrinsicCallable
|
const intrinsicBufferByteLength = Reflect.get(Buffer, 'byteLength') as IntrinsicCallable
|
||||||
|
const intrinsicObjectCreate = Object.create
|
||||||
const intrinsicObjectDefineProperty = Object.defineProperty
|
const intrinsicObjectDefineProperty = Object.defineProperty
|
||||||
const intrinsicObjectKeys = Object.keys
|
const intrinsicObjectKeys = Object.keys
|
||||||
const intrinsicString = String
|
const intrinsicString = String
|
||||||
@@ -19,6 +20,22 @@ const intrinsicStringCharCodeAt = Reflect.get(String.prototype, 'charCodeAt') as
|
|||||||
const intrinsicStringCodePointAt = Reflect.get(String.prototype, 'codePointAt') as IntrinsicCallable
|
const intrinsicStringCodePointAt = Reflect.get(String.prototype, 'codePointAt') as IntrinsicCallable
|
||||||
const intrinsicStringSlice = Reflect.get(String.prototype, 'slice') as IntrinsicCallable
|
const intrinsicStringSlice = Reflect.get(String.prototype, 'slice') as IntrinsicCallable
|
||||||
|
|
||||||
|
/** Build a data descriptor that cannot inherit model-defined accessor fields. */
|
||||||
|
function dataDescriptor(value: unknown): PropertyDescriptor {
|
||||||
|
const descriptor = intrinsicObjectCreate(null) as PropertyDescriptor
|
||||||
|
descriptor.value = value
|
||||||
|
return descriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
|
||||||
|
function defineEnumerableDataProperty(target: object, key: PropertyKey, value: unknown): void {
|
||||||
|
const descriptor = dataDescriptor(value)
|
||||||
|
descriptor.enumerable = true
|
||||||
|
descriptor.configurable = true
|
||||||
|
descriptor.writable = true
|
||||||
|
intrinsicObjectDefineProperty(target, key, descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
/** UTF-8 byte length through the module-captured Node intrinsic. */
|
/** UTF-8 byte length through the module-captured Node intrinsic. */
|
||||||
function byteLength(text: string): number {
|
function byteLength(text: string): number {
|
||||||
return intrinsicReflectApply(intrinsicBufferByteLength, IntrinsicBuffer, [text, 'utf8']) as number
|
return intrinsicReflectApply(intrinsicBufferByteLength, IntrinsicBuffer, [text, 'utf8']) as number
|
||||||
@@ -26,12 +43,7 @@ function byteLength(text: string): number {
|
|||||||
|
|
||||||
/** Append without consulting a model-mutated `Array.prototype`. */
|
/** Append without consulting a model-mutated `Array.prototype`. */
|
||||||
function append<T>(target: T[], value: T): void {
|
function append<T>(target: T[], value: T): void {
|
||||||
intrinsicObjectDefineProperty(target, target.length, {
|
defineEnumerableDataProperty(target, target.length, value)
|
||||||
value,
|
|
||||||
enumerable: true,
|
|
||||||
configurable: true,
|
|
||||||
writable: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pop without consulting a model-mutated `Array.prototype`. */
|
/** Pop without consulting a model-mutated `Array.prototype`. */
|
||||||
@@ -39,7 +51,7 @@ function takeLast<T>(target: T[]): T | undefined {
|
|||||||
if (target.length === 0) return undefined
|
if (target.length === 0) return undefined
|
||||||
const index = target.length - 1
|
const index = target.length - 1
|
||||||
const value = target[index]
|
const value = target[index]
|
||||||
intrinsicObjectDefineProperty(target, 'length', { value: index })
|
intrinsicObjectDefineProperty(target, 'length', dataDescriptor(index))
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,28 +14,42 @@ const intrinsicReflectApply = Reflect.get(Reflect, 'apply') as (
|
|||||||
const IntrinsicError = Error
|
const IntrinsicError = Error
|
||||||
const IntrinsicSet = Set
|
const IntrinsicSet = Set
|
||||||
const intrinsicArrayIsArray = Array.isArray
|
const intrinsicArrayIsArray = Array.isArray
|
||||||
|
const intrinsicArrayPrototype = Array.prototype
|
||||||
const intrinsicNumberIsFinite = Number.isFinite
|
const intrinsicNumberIsFinite = Number.isFinite
|
||||||
const intrinsicNumberIsSafeInteger = Number.isSafeInteger
|
const intrinsicNumberIsSafeInteger = Number.isSafeInteger
|
||||||
|
const intrinsicObjectCreate = Object.create
|
||||||
const intrinsicObjectDefineProperty = Object.defineProperty
|
const intrinsicObjectDefineProperty = Object.defineProperty
|
||||||
const intrinsicObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor
|
const intrinsicObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor
|
||||||
const intrinsicObjectGetPrototypeOf = Object.getPrototypeOf
|
const intrinsicObjectGetPrototypeOf = Object.getPrototypeOf
|
||||||
const intrinsicObjectHasOwn = Object.hasOwn
|
const intrinsicObjectHasOwn = Object.hasOwn
|
||||||
const intrinsicObjectIs = Object.is
|
const intrinsicObjectIs = Object.is
|
||||||
const intrinsicObjectKeys = Object.keys
|
const intrinsicObjectKeys = Object.keys
|
||||||
const intrinsicObjectPropertyIsEnumerable = Reflect.get(Object.prototype, 'propertyIsEnumerable') as IntrinsicCallable
|
const intrinsicObjectPrototype = Object.prototype
|
||||||
|
const intrinsicObjectPropertyIsEnumerable = Reflect.get(intrinsicObjectPrototype, 'propertyIsEnumerable') as IntrinsicCallable
|
||||||
const intrinsicReflectOwnKeys = Reflect.ownKeys
|
const intrinsicReflectOwnKeys = Reflect.ownKeys
|
||||||
const intrinsicSetAdd = Reflect.get(Set.prototype, 'add') as IntrinsicCallable
|
const intrinsicSetAdd = Reflect.get(Set.prototype, 'add') as IntrinsicCallable
|
||||||
const intrinsicSetDelete = Reflect.get(Set.prototype, 'delete') as IntrinsicCallable
|
const intrinsicSetDelete = Reflect.get(Set.prototype, 'delete') as IntrinsicCallable
|
||||||
const intrinsicSetHas = Reflect.get(Set.prototype, 'has') as IntrinsicCallable
|
const intrinsicSetHas = Reflect.get(Set.prototype, 'has') as IntrinsicCallable
|
||||||
|
|
||||||
|
/** Build a data descriptor that cannot inherit model-defined accessor fields. */
|
||||||
|
function dataDescriptor(value: unknown): PropertyDescriptor {
|
||||||
|
const descriptor = intrinsicObjectCreate(null) as PropertyDescriptor
|
||||||
|
descriptor.value = value
|
||||||
|
return descriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
|
||||||
|
function defineEnumerableDataProperty(target: object, key: PropertyKey, value: unknown): void {
|
||||||
|
const descriptor = dataDescriptor(value)
|
||||||
|
descriptor.enumerable = true
|
||||||
|
descriptor.configurable = true
|
||||||
|
descriptor.writable = true
|
||||||
|
intrinsicObjectDefineProperty(target, key, descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
/** Append without consulting a model-mutated `Array.prototype`. */
|
/** Append without consulting a model-mutated `Array.prototype`. */
|
||||||
function append<T>(target: T[], value: T): void {
|
function append<T>(target: T[], value: T): void {
|
||||||
intrinsicObjectDefineProperty(target, target.length, {
|
defineEnumerableDataProperty(target, target.length, value)
|
||||||
value,
|
|
||||||
enumerable: true,
|
|
||||||
configurable: true,
|
|
||||||
writable: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pop without consulting a model-mutated `Array.prototype`. */
|
/** Pop without consulting a model-mutated `Array.prototype`. */
|
||||||
@@ -43,7 +57,7 @@ function takeLast<T>(target: T[]): T | undefined {
|
|||||||
if (target.length === 0) return undefined
|
if (target.length === 0) return undefined
|
||||||
const index = target.length - 1
|
const index = target.length - 1
|
||||||
const value = target[index]
|
const value = target[index]
|
||||||
intrinsicObjectDefineProperty(target, 'length', { value: index })
|
intrinsicObjectDefineProperty(target, 'length', dataDescriptor(index))
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,26 +90,28 @@ function hasIntrinsicConstructor(prototype: object, name: 'Array' | 'Object'): b
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether a candidate is one realm's intrinsic `Object.prototype`. */
|
/** Whether a candidate is a foreign realm's intrinsic `Object.prototype`. */
|
||||||
function isIntrinsicObjectPrototype(value: object): boolean {
|
function isForeignIntrinsicObjectPrototype(value: object): boolean {
|
||||||
return intrinsicObjectGetPrototypeOf(value) === null && hasIntrinsicConstructor(value, 'Object')
|
return intrinsicObjectGetPrototypeOf(value) === null && hasIntrinsicConstructor(value, 'Object')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
|
/** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
|
||||||
function hasPlainArrayPrototype(value: unknown[]): boolean {
|
function hasPlainArrayPrototype(value: unknown[]): boolean {
|
||||||
const prototype: unknown = intrinsicObjectGetPrototypeOf(value)
|
const prototype: unknown = intrinsicObjectGetPrototypeOf(value)
|
||||||
|
if (prototype === intrinsicArrayPrototype) return true
|
||||||
if (!intrinsicArrayIsArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
|
if (!intrinsicArrayIsArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
|
||||||
const objectPrototype: unknown = intrinsicObjectGetPrototypeOf(prototype)
|
const objectPrototype: unknown = intrinsicObjectGetPrototypeOf(prototype)
|
||||||
return typeof objectPrototype === 'object'
|
return typeof objectPrototype === 'object'
|
||||||
&& objectPrototype !== null
|
&& objectPrototype !== null
|
||||||
&& isIntrinsicObjectPrototype(objectPrototype)
|
&& isForeignIntrinsicObjectPrototype(objectPrototype)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether an object is a plain or null-prototype record from any JavaScript realm. */
|
/** Whether an object is a plain or null-prototype record from any JavaScript realm. */
|
||||||
function hasPlainObjectPrototype(value: object): boolean {
|
function hasPlainObjectPrototype(value: object): boolean {
|
||||||
const prototype: unknown = intrinsicObjectGetPrototypeOf(value)
|
const prototype: unknown = intrinsicObjectGetPrototypeOf(value)
|
||||||
return prototype === null
|
return prototype === null
|
||||||
|| typeof prototype === 'object' && isIntrinsicObjectPrototype(prototype)
|
|| prototype === intrinsicObjectPrototype
|
||||||
|
|| typeof prototype === 'object' && isForeignIntrinsicObjectPrototype(prototype)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return every JSON-visible object key, or reject own data JSON would discard. */
|
/** Return every JSON-visible object key, or reject own data JSON would discard. */
|
||||||
@@ -135,19 +151,9 @@ export function snapshotCodeJsonValue(value: unknown): CodeJsonValue | undefined
|
|||||||
if (destination.kind === 'root') {
|
if (destination.kind === 'root') {
|
||||||
root = item
|
root = item
|
||||||
} else if (destination.kind === 'array') {
|
} else if (destination.kind === 'array') {
|
||||||
intrinsicObjectDefineProperty(destination.target, destination.index, {
|
defineEnumerableDataProperty(destination.target, destination.index, item)
|
||||||
value: item,
|
|
||||||
enumerable: true,
|
|
||||||
configurable: true,
|
|
||||||
writable: true,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
intrinsicObjectDefineProperty(destination.target, destination.key, {
|
defineEnumerableDataProperty(destination.target, destination.key, item)
|
||||||
value: item,
|
|
||||||
enumerable: true,
|
|
||||||
configurable: true,
|
|
||||||
writable: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,12 +367,7 @@ export function decodeWorkerJson(input: unknown): CodeJsonValue | undefined {
|
|||||||
const key = parent.keys[parent.index]
|
const key = parent.keys[parent.index]
|
||||||
/* v8 ignore next -- object frames are built from validated keys and their exact length. */
|
/* v8 ignore next -- object frames are built from validated keys and their exact length. */
|
||||||
if (key === undefined) return false
|
if (key === undefined) return false
|
||||||
intrinsicObjectDefineProperty(parent.target, key, {
|
defineEnumerableDataProperty(parent.target, key, value)
|
||||||
value,
|
|
||||||
enumerable: true,
|
|
||||||
configurable: true,
|
|
||||||
writable: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
parent.index += 1
|
parent.index += 1
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -677,6 +677,8 @@ describe('WorkerCodeRuntime — hostile programs (real workers)', () => {
|
|||||||
stringPrototype.charCodeAt = stringPrototype.codePointAt = stringPrototype.slice = () => { throw new Error('mutated string method') };
|
stringPrototype.charCodeAt = stringPrototype.codePointAt = stringPrototype.slice = () => { throw new Error('mutated string method') };
|
||||||
Buffer.byteLength = () => 0;
|
Buffer.byteLength = () => 0;
|
||||||
Function.prototype.toString = () => 'mutated';
|
Function.prototype.toString = () => 'mutated';
|
||||||
|
objectPrototype.get = () => undefined;
|
||||||
|
objectPrototype.constructor = arrayPrototype.constructor = null;
|
||||||
globalThis.Array = globalThis.Buffer = globalThis.Function = globalThis.Number = globalThis.Object = globalThis.Reflect = globalThis.Set = globalThis.String = undefined;
|
globalThis.Array = globalThis.Buffer = globalThis.Function = globalThis.Number = globalThis.Object = globalThis.Reflect = globalThis.Set = globalThis.String = undefined;
|
||||||
const echoed = await tools.echo({ request: ['€', 1] });
|
const echoed = await tools.echo({ request: ['€', 1] });
|
||||||
return { echoed, completion: { ok: true, amount: 42 } };
|
return { echoed, completion: { ok: true, amount: 42 } };
|
||||||
|
|||||||
Reference in New Issue
Block a user