Skip to content

Commit d9a1fde

Browse files
authored
Upstream security fixes, and update supported node.js versions (#540)
* make it possible identify proxies by checking `if (obj.isProxy)` * fix a typo * backport security fixes, and add additional tests * upgrade GH actions, and drop unsupported versions of node.js
1 parent b51d33c commit d9a1fde

File tree

5 files changed

+112
-87
lines changed

5 files changed

+112
-87
lines changed

.github/workflows/node-test.yml

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,13 @@ jobs:
99
runs-on: ubuntu-latest
1010
strategy:
1111
matrix:
12-
node-version: [8, 10, 12, 14, 16, 18, 19]
12+
node-version: [18, 20, 22]
1313
steps:
14-
- uses: actions/checkout@v3
14+
- uses: actions/checkout@v4
1515
- name: Use Node.js ${{ matrix.node-version }}
16-
uses: actions/setup-node@v3
16+
uses: actions/setup-node@v4
1717
with:
1818
node-version: ${{ matrix.node-version }}
1919
- name: Install dependencies
2020
run: npm ci
2121
- run: npm test
22-
23-
# It seems that npm for node v6 does not have the ci command.
24-
testv6:
25-
runs-on: ubuntu-latest
26-
steps:
27-
- uses: actions/checkout@v3
28-
- name: Use Node.js 6
29-
uses: actions/setup-node@v3
30-
with:
31-
node-version: 6
32-
- name: Install dependencies
33-
run: npm install
34-
- run: npm test

lib/bridge.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,18 +158,19 @@ function thisIdMapping(factory, other) {
158158
const thisThrowOnKeyAccessHandler = thisObjectFreeze({
159159
__proto__: null,
160160
get(target, key, receiver) {
161+
if (key === 'isProxy') return true;
161162
if (typeof key === 'symbol') {
162163
key = thisReflectApply(thisSymbolToString, key, []);
163164
}
164165
throw new VMError(`Unexpected access to key '${key}'`);
165166
}
166167
});
167168

168-
const emptyForzenObject = thisObjectFreeze({
169+
const emptyFrozenObject = thisObjectFreeze({
169170
__proto__: null
170171
});
171172

172-
const thisThrowOnKeyAccess = new ThisProxy(emptyForzenObject, thisThrowOnKeyAccessHandler);
173+
const thisThrowOnKeyAccess = new ThisProxy(emptyFrozenObject, thisThrowOnKeyAccessHandler);
173174

174175
function SafeBase() {}
175176

@@ -413,12 +414,16 @@ function createBridge(otherInit, registerProxy) {
413414
}
414415

415416
get(target, key, receiver) {
417+
if (key === 'isProxy') return true;
416418
// Note: target@this(unsafe) key@prim receiver@this(unsafe) throws@this(unsafe)
417419
const object = this.getObject(); // @other(unsafe)
418420
switch (key) {
419421
case 'constructor': {
420422
const desc = otherSafeGetOwnPropertyDescriptor(object, key);
421-
if (desc) return thisDefaultGet(this, object, key, desc);
423+
if (desc) {
424+
if (desc.value && desc.value.name === 'Function') return {};
425+
return thisDefaultGet(this, object, key, desc);
426+
}
422427
const proto = thisReflectGetPrototypeOf(target);
423428
return proto === null ? undefined : proto.constructor;
424429
}
@@ -782,6 +787,7 @@ function createBridge(otherInit, registerProxy) {
782787
}
783788

784789
get(target, key, receiver) {
790+
if (key === 'isProxy') return true;
785791
// Note: target@this(unsafe) key@prim receiver@this(unsafe) throws@this(unsafe)
786792
const object = this.getObject(); // @other(unsafe)
787793
const mock = this.mock;
@@ -812,7 +818,7 @@ function createBridge(otherInit, registerProxy) {
812818
thisReflectApply(thisWeakMapSet, mappingOtherToThis, [other, proxy]);
813819
return proxy;
814820
}
815-
const proxy2 = new ThisProxy(proxy, emptyForzenObject);
821+
const proxy2 = new ThisProxy(proxy, emptyFrozenObject);
816822
try {
817823
otherReflectApply(otherWeakMapSet, mappingThisToOther, [proxy2, other]);
818824
registerProxy(proxy2, handler);

lib/setup-sandbox.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ const {
1010
Proxy: LocalProxy,
1111
WeakMap: LocalWeakMap,
1212
Function: localFunction,
13-
Promise: localPromise,
1413
eval: localEval
1514
} = global;
1615

@@ -20,7 +19,7 @@ const {
2019

2120
const {
2221
getPrototypeOf: localReflectGetPrototypeOf,
23-
apply: localReflectApply,
22+
apply,
2423
construct: localReflectConstruct,
2524
deleteProperty: localReflectDeleteProperty,
2625
has: localReflectHas,
@@ -29,6 +28,27 @@ const {
2928
getOwnPropertyDescriptor: localReflectGetOwnPropertyDescriptor
3029
} = localReflect;
3130

31+
const speciesSymbol = Symbol.species;
32+
const globalPromise = global.Promise;
33+
class localPromise extends globalPromise {}
34+
35+
const resetPromiseSpecies = (p) => {
36+
if (p instanceof globalPromise && ![globalPromise, localPromise].includes(p.constructor[speciesSymbol])) {
37+
Object.defineProperty(p.constructor, speciesSymbol, { value: localPromise });
38+
}
39+
};
40+
41+
const globalPromiseThen = globalPromise.prototype.then;
42+
globalPromise.prototype.then = function then(onFulfilled, onRejected) {
43+
resetPromiseSpecies(this);
44+
return globalPromiseThen.call(this, onFulfilled, onRejected);
45+
};
46+
47+
const localReflectApply = (target, thisArg, args) => {
48+
resetPromiseSpecies(thisArg);
49+
return apply(target, thisArg, args);
50+
};
51+
3252
const {
3353
isArray: localArrayIsArray
3454
} = localArray;
@@ -69,7 +89,9 @@ Object.defineProperties(global, {
6989
globalThis: {value: global, writable: true, configurable: true},
7090
GLOBAL: {value: global, writable: true, configurable: true},
7191
root: {value: global, writable: true, configurable: true},
72-
Error: {value: LocalError}
92+
Error: {value: LocalError},
93+
Promise: {value: localPromise},
94+
Proxy: {value: undefined}
7395
});
7496

7597
if (!localReflectDefineProperty(global, 'VMError', {
@@ -462,6 +484,7 @@ const makeSafeArgs = Object.freeze({
462484
const proxyHandlerHandler = Object.freeze({
463485
__proto__: null,
464486
get(target, name, receiver) {
487+
if (name === 'isProxy') return true;
465488
const value = target.handler[name];
466489
if (typeof value !== 'function') return value;
467490
return new LocalProxy(value, makeSafeArgs);
@@ -529,8 +552,16 @@ if (localPromise) {
529552

530553
}
531554

555+
Object.freeze(localPromise);
556+
Object.freeze(PromisePrototype);
532557
}
533558

559+
localObject.defineProperty(localObject, 'setPrototypeOf', {
560+
value: () => {
561+
throw new VMError('Operation not allowed on contextified object.');
562+
}
563+
});
564+
534565
function readonly(other, mock) {
535566
// Note: other@other(unsafe) mock@other(unsafe) returns@this(unsafe) throws@this(unsafe)
536567
if (!mock) return fromWithFactory(readonlyFactory, other);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"mocha": "^6.2.2"
2929
},
3030
"engines": {
31-
"node": ">=6.0"
31+
"node": ">=18.0"
3232
},
3333
"scripts": {
3434
"test": "mocha test",

test/vm.js

Lines changed: 64 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ function makeHelpers() {
4242
function addProp(o, name, path) {
4343
const prop = Object.getOwnPropertyDescriptor(o, name);
4444
if (typeof name === 'symbol') name = '!' + name.toString();
45-
Object.setPrototypeOf(prop, null);
4645
addObj(prop.get, `${path}>${name}`);
4746
addObj(prop.set, `${path}<${name}`);
4847
addObj(prop.value, `${path}.${name}`);
@@ -622,7 +621,7 @@ describe('VM', () => {
622621
if (o && o.constructor !== Function) throw new Error('Shouldnt be there.');
623622
`), '#3');
624623

625-
assert.doesNotThrow(() => vm2.run(`
624+
assert.throws(() => vm2.run(`
626625
let method = () => {};
627626
let proxy = new Proxy(method, {
628627
apply: (target, context, args) => {
@@ -631,16 +630,16 @@ describe('VM', () => {
631630
}
632631
});
633632
proxy
634-
`)('asdf'), '#4');
633+
`)('asdf'), /Proxy is not a constructor/, '#4');
635634

636-
assert.doesNotThrow(() => vm2.run(`
635+
assert.throws(() => vm2.run(`
637636
let proxy2 = new Proxy(function() {}, {
638637
apply: (target, context, args) => {
639638
if (args.constructor.constructor !== Function) throw new Error('Shouldnt be there.');
640639
}
641640
});
642641
proxy2
643-
`)('asdf'), '#5');
642+
`)('asdf'), /Proxy is not a constructor/, '#5');
644643

645644
assert.strictEqual(vm2.run(`
646645
global.DEBUG = true;
@@ -674,7 +673,7 @@ describe('VM', () => {
674673
} catch ({constructor: c}) {
675674
c.constructor('return process')();
676675
}
677-
`), /Maximum call stack size exceeded/, '#9');
676+
`), /Proxy is not a constructor/, '#9');
678677
});
679678

680679
it('internal state attack', () => {
@@ -729,21 +728,15 @@ describe('VM', () => {
729728
}
730729
});
731730

732-
try {
733-
vm2.run(`
734-
func(() => {
735-
throw new Proxy({}, {
736-
getPrototypeOf: () => {
737-
throw x => x.constructor.constructor("return process;")();
738-
}
739-
})
740-
});
741-
`);
742-
} catch (ex) {
743-
assert.throws(()=>{
744-
ex(()=>{});
745-
}, /process is not defined/);
746-
}
731+
assert.throws(() => vm2.run(`
732+
func(() => {
733+
throw new Proxy({}, {
734+
getPrototypeOf: () => {
735+
throw x => x.constructor.constructor("return process;")();
736+
}
737+
})
738+
});
739+
`), /Proxy is not a constructor/);
747740
});
748741

749742
it('__defineGetter__ / __defineSetter__ attack', () => {
@@ -815,40 +808,11 @@ describe('VM', () => {
815808
return () => x => x.constructor("return process")();
816809
}
817810
})))(()=>{}).mainModule.require("child_process").execSync("id").toString()
818-
`), /process is not defined/, '#2');
811+
`), /Proxy is not a constructor/, '#2');
819812

820813
vm2 = new VM();
821814

822815
assert.throws(() => vm2.run(`
823-
var process;
824-
try {
825-
Object.defineProperty(Buffer.from(""), "y", {
826-
writable: true,
827-
value: new Proxy({}, {
828-
getPrototypeOf(target) {
829-
delete this.getPrototypeOf;
830-
831-
Object.defineProperty(Object.prototype, "get", {
832-
get() {
833-
delete Object.prototype.get;
834-
Function.prototype.__proto__ = null;
835-
throw f=>f.constructor("return process")();
836-
}
837-
});
838-
839-
return Object.getPrototypeOf(target);
840-
}
841-
})
842-
});
843-
} catch(e) {
844-
process = e(() => {});
845-
}
846-
process.mainModule.require("child_process").execSync("whoami").toString()
847-
`), /Cannot read propert.*mainModule/, '#3');
848-
849-
vm2 = new VM();
850-
851-
assert.doesNotThrow(() => vm2.run(`
852816
Object.defineProperty(Buffer.from(""), "", {
853817
value: new Proxy({}, {
854818
getPrototypeOf(target) {
@@ -861,7 +825,7 @@ describe('VM', () => {
861825
}
862826
})
863827
});
864-
`), '#4');
828+
`), /Proxy is not a constructor/, '#4');
865829

866830
vm2 = new VM();
867831

@@ -988,7 +952,7 @@ describe('VM', () => {
988952
}
989953
}
990954
}))}).mainModule.require("child_process").execSync("id").toString()
991-
`), /process is not defined/, '#1');
955+
`), /Proxy is not a constructor/, '#1');
992956
});
993957

994958
it('throw while accessing propertyDescriptor properties', () => {
@@ -1045,17 +1009,13 @@ describe('VM', () => {
10451009

10461010
assert.throws(() => vm2.run(`
10471011
(function(){
1048-
try{
1049-
Buffer.from(new Proxy({}, {
1050-
getOwnPropertyDescriptor(){
1051-
throw f=>f.constructor("return process")();
1052-
}
1053-
}));
1054-
}catch(e){
1055-
return e(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
1056-
}
1012+
Buffer.from(new Proxy({}, {
1013+
getOwnPropertyDescriptor(){
1014+
throw f=>f.constructor("return process")();
1015+
}
1016+
}));
10571017
})()
1058-
`), /process is not defined/);
1018+
`), /Proxy is not a constructor/);
10591019
});
10601020

10611021
if (NODE_VERSION >= 10) {
@@ -1160,6 +1120,47 @@ describe('VM', () => {
11601120
`), /process is not defined/);
11611121
});
11621122

1123+
it('allow regular async functions', async () => {
1124+
const vm2 = new VM();
1125+
const promise = vm2.run(`(async () => 42)()`);
1126+
assert.strictEqual(await promise, 42);
1127+
});
1128+
1129+
it('allow regular promises', async () => {
1130+
const vm2 = new VM();
1131+
const promise = vm2.run(`new Promise((resolve) => resolve(42))`);
1132+
assert.strictEqual(await promise, 42);
1133+
});
1134+
1135+
it('[Symbol.species] attack', async () => {
1136+
const vm2 = new VM();
1137+
const promise = vm2.run(`
1138+
async function fn() {
1139+
throw new Error('random error');
1140+
}
1141+
const promise = fn();
1142+
promise.constructor = {
1143+
[Symbol.species]: class WrappedPromise {
1144+
constructor(executor) {
1145+
executor(() => 43, () => 44);
1146+
}
1147+
}
1148+
};
1149+
promise.then();
1150+
`);
1151+
assert.rejects(() => promise, /random error/);
1152+
});
1153+
1154+
it('constructor arbitrary code attack', async () => {
1155+
const vm2 = new VM();
1156+
assert.throws(()=>vm2.run(`
1157+
const g = ({}).__lookupGetter__;
1158+
const a = Buffer.apply;
1159+
const p = a.apply(g, [Buffer, ['__proto__']]);
1160+
p.call(a).constructor('return process')();
1161+
`), /constructor is not a function/);
1162+
});
1163+
11631164
after(() => {
11641165
vm = null;
11651166
});

0 commit comments

Comments
 (0)