@@ -36,6 +36,32 @@ const ReflectConstruct = Reflect.construct;
3636// than whatever the `Reflect` binding resolves to when the safe runtime is assembled.
3737const ReflectObject = Reflect ;
3838
39+ /**
40+ * Snapshot of every own value on the pristine `Reflect` namespace, taken at module load.
41+ *
42+ * `createSafeReflect` serves reads from this snapshot instead of re-reading the namespace, so a
43+ * later `Reflect.get = evil` in a shared realm cannot be observed through the sandbox's Reflect.
44+ * Pinning the namespace object alone is not enough: the proxy target's own properties stay
45+ * writable, so only a value snapshot closes the window. Every own property of `Reflect` is a data
46+ * property (the methods plus `Symbol.toStringTag`), so building the snapshot triggers no getters.
47+ */
48+ const PINNED_REFLECT_VALUES = new Map < string | symbol , unknown > ( ) ;
49+ for ( const key of ReflectOwnKeys ( ReflectObject ) ) {
50+ PINNED_REFLECT_VALUES . set ( key , ReflectGet ( ReflectObject , key ) ) ;
51+ }
52+
53+ // Function constructors, pinned at module load. `createSafeReflect`'s construct guard compares
54+ // against these: reading them live (`async function () {}.constructor`) walks a prototype whose
55+ // `constructor` is writable, so a shared-realm attacker could point the comparison at a decoy and
56+ // slip the real Function constructor past the check.
57+ const FunctionCtor = Function ;
58+ // Intentional empty functions used only to reach the hidden function-constructor intrinsics.
59+ /* eslint-disable @typescript-eslint/no-empty-function */
60+ const AsyncFunctionCtor = async function ( ) { } . constructor ;
61+ const GeneratorFunctionCtor = function * ( ) { } . constructor ;
62+ const AsyncGeneratorFunctionCtor = async function * ( ) { } . constructor ;
63+ /* eslint-enable @typescript-eslint/no-empty-function */
64+
3965/**
4066 * Absolute floor for the membrane recursion cap.
4167 *
@@ -259,33 +285,36 @@ export function createSafeReflect(securityLevel: SecurityLevel): typeof Reflect
259285 }
260286
261287 return new Proxy ( ReflectObject , {
262- get ( target , prop : string | symbol ) {
288+ get ( _target , prop : string | symbol ) {
263289 if ( typeof prop === 'string' && dangerousMethods . has ( prop ) ) {
264290 return undefined ;
265291 }
266292
267- const value = ReflectGet ( target , prop ) ;
293+ // Serve from the module-load snapshot: the proxy target's own properties are writable, so
294+ // re-reading them here would expose whatever the current realm holds. Keys absent from the
295+ // snapshot were not on the pristine namespace and are therefore never served.
296+ if ( ! PINNED_REFLECT_VALUES . has ( prop ) ) {
297+ return undefined ;
298+ }
299+ const value = PINNED_REFLECT_VALUES . get ( prop ) ;
268300
269301 // Wrap Reflect.construct to block Function constructors
270302 if ( typeof value === 'function' && prop === 'construct' ) {
271303 const safeConstruct = function ( ctorTarget : unknown , args : unknown [ ] , newTarget ?: unknown ) {
272304 // Block Function, AsyncFunction, GeneratorFunction, AsyncGeneratorFunction constructors
273- // Intentional empty functions to obtain constructor references
274- // eslint-disable-next-line @typescript-eslint/no-empty-function
275- const AsyncFunction = async function ( ) { } . constructor ;
276- // eslint-disable-next-line @typescript-eslint/no-empty-function
277- const GeneratorFunction = function * ( ) { } . constructor ;
278- // eslint-disable-next-line @typescript-eslint/no-empty-function
279- const AsyncGeneratorFunction = async function * ( ) { } . constructor ;
280-
281305 if (
282- ctorTarget === Function ||
283- ctorTarget === AsyncFunction ||
284- ctorTarget === GeneratorFunction ||
285- ctorTarget === AsyncGeneratorFunction
306+ ctorTarget === FunctionCtor ||
307+ ctorTarget === AsyncFunctionCtor ||
308+ ctorTarget === GeneratorFunctionCtor ||
309+ ctorTarget === AsyncGeneratorFunctionCtor
286310 ) {
287311 throw createSafeError ( 'Reflect.construct with function constructors is blocked' , 'SecurityError' ) ;
288312 }
313+ // `Reflect.construct(t, a, undefined)` throws — newTarget is "present" as soon as a
314+ // third argument is passed. Forward the 2-argument form when the caller omitted it.
315+ if ( newTarget === undefined ) {
316+ return ReflectConstruct ( ctorTarget as new ( ...args : unknown [ ] ) => unknown , args as unknown [ ] ) ;
317+ }
289318 return ReflectConstruct (
290319 ctorTarget as new ( ...args : unknown [ ] ) => unknown ,
291320 args as unknown [ ] ,
0 commit comments