Skip to content

Commit 9084cd6

Browse files
committed
fix: block host Function constructor leak via direct handler.get() call
1 parent 6c194d9 commit 9084cd6

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

lib/bridge.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,39 @@ try {
7878
thisGlobalPrototypes['AsyncGeneratorFunction'] = eval('(async function*() {})').constructor.prototype;
7979
} catch (e) {}
8080

81+
// Cache this-realm dangerous function constructors.
82+
// Used to block raw host Function constructors from leaking when handler
83+
// methods are called directly (e.g., via showProxy handler exposure).
84+
// This complements isDangerousFunctionConstructor which checks OTHER-realm constructors.
85+
let thisAsyncFunctionCtor;
86+
let thisGeneratorFunctionCtor;
87+
let thisAsyncGeneratorFunctionCtor;
88+
try {
89+
if (thisGlobalPrototypes.AsyncFunction) {
90+
const desc = thisReflectGetOwnPropertyDescriptor(thisGlobalPrototypes.AsyncFunction, 'constructor');
91+
if (desc) thisAsyncFunctionCtor = desc.value;
92+
}
93+
} catch (e) {}
94+
try {
95+
if (thisGlobalPrototypes.GeneratorFunction) {
96+
const desc = thisReflectGetOwnPropertyDescriptor(thisGlobalPrototypes.GeneratorFunction, 'constructor');
97+
if (desc) thisGeneratorFunctionCtor = desc.value;
98+
}
99+
} catch (e) {}
100+
try {
101+
if (thisGlobalPrototypes.AsyncGeneratorFunction) {
102+
const desc = thisReflectGetOwnPropertyDescriptor(thisGlobalPrototypes.AsyncGeneratorFunction, 'constructor');
103+
if (desc) thisAsyncGeneratorFunctionCtor = desc.value;
104+
}
105+
} catch (e) {}
106+
107+
function isThisDangerousFunctionConstructor(value) {
108+
return value === thisFunction ||
109+
(thisAsyncFunctionCtor && value === thisAsyncFunctionCtor) ||
110+
(thisGeneratorFunctionCtor && value === thisGeneratorFunctionCtor) ||
111+
(thisAsyncGeneratorFunctionCtor && value === thisAsyncGeneratorFunctionCtor);
112+
}
113+
81114
const {
82115
getPrototypeOf: thisReflectGetPrototypeOf,
83116
setPrototypeOf: thisReflectSetPrototypeOf,
@@ -566,7 +599,15 @@ function createBridge(otherInit, registerProxy) {
566599
return thisDefaultGet(this, object, key, desc);
567600
}
568601
const proto = thisReflectGetPrototypeOf(target);
569-
return proto === null ? undefined : proto.constructor;
602+
if (proto === null) return undefined;
603+
const ctor = proto.constructor;
604+
// Defense in depth: block this-realm dangerous function constructors.
605+
// Normally handler methods are only called by the proxy mechanism
606+
// which handles return values safely, but if the handler is exposed
607+
// (e.g., via util.inspect showProxy), attackers can call get()
608+
// directly with a forged target, leaking raw host constructors.
609+
if (isThisDangerousFunctionConstructor(ctor)) return {};
610+
return ctor;
570611
}
571612
case '__proto__': {
572613
const desc = otherSafeGetOwnPropertyDescriptor(object, key);

test/vm.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2299,6 +2299,45 @@ describe('VM', () => {
22992299
assert.strictEqual(attackResult, false, 'getFactory should not be accessible on handler');
23002300
});
23012301

2302+
it('Handler get() direct call with forged target leaking host Function constructor', () => {
2303+
// This attack uses showProxy to obtain a handler reference, then calls
2304+
// handler.get() directly via a host-side reduce chain with a raw host
2305+
// function as the target argument. The 'constructor' case falls through
2306+
// to thisReflectGetPrototypeOf(target) → Function.prototype.constructor,
2307+
// leaking the raw host Function constructor.
2308+
// The fix adds isThisDangerousFunctionConstructor check on the return value.
2309+
const vm2 = new VM();
2310+
const attackResult = vm2.run(`
2311+
const g = ({}).__lookupGetter__;
2312+
const a = Buffer.apply;
2313+
const p = a.apply(g, [Buffer, ['__proto__']]);
2314+
const op = p.call(p.call(p.call(p.call(Buffer.of()))));
2315+
const ho = op.constructor;
2316+
const obj = {
2317+
subarray: Buffer.prototype.inspect,
2318+
slice: Buffer.prototype.slice,
2319+
hexSlice:()=>''
2320+
};
2321+
let f;
2322+
obj.slice(20, {showHidden: true, showProxy: true, depth: 10, stylize(a) {
2323+
if (this.seen?.[1]?.get){f=this.seen[1];}
2324+
return a;
2325+
}});
2326+
let escaped = false;
2327+
try {
2328+
const b = ho.entries({});
2329+
b[0] = [f, [obj.slice, 'constructor']];
2330+
b[1] = [undefined, ["return process"]];
2331+
const proc = b.reduce(a.apply(a.bind, [a, [a]]), f.get)();
2332+
if (proc && typeof proc === 'object') escaped = true;
2333+
} catch (e) {
2334+
// Expected: fails because {} is returned instead of Function
2335+
}
2336+
escaped;
2337+
`);
2338+
assert.strictEqual(attackResult, false, 'Handler get() should not leak host Function constructor via forged target');
2339+
});
2340+
23022341
// Promise.try is available in Node.js 24+
23032342
// This is the ONLY Promise static method that is actually vulnerable because:
23042343
// - Promise.try catches errors thrown by the callback INSIDE V8's Promise executor

0 commit comments

Comments
 (0)