Skip to content
This repository was archived by the owner on Aug 18, 2022. It is now read-only.

Commit 48d376a

Browse files
author
kage-sakura
authored
Protect outer objects (#70)
1 parent 1d2e803 commit 48d376a

File tree

2 files changed

+144
-54
lines changed

2 files changed

+144
-54
lines changed

src/setup.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
Object.defineProperty(
2+
RegExp.prototype,
3+
Symbol.for('nodejs.util.inspect.custom'),
4+
{
5+
value: RegExp.prototype.toString,
6+
}
7+
)
8+
9+
{
10+
const { isNaN } = Number,
11+
{ prototype } = Date,
12+
{ call } = Function.prototype,
13+
getTime = call.bind(prototype.getTime),
14+
toString = call.bind(prototype.toString),
15+
toISOString = call.bind(prototype.toISOString)
16+
Object.defineProperty(prototype, Symbol.for('nodejs.util.inspect.custom'), {
17+
value() {
18+
return isNaN(getTime(this)) ? toString(this) : toISOString(this)
19+
},
20+
})
21+
}
22+
23+
{
24+
const call = Function.prototype.call.bind(Function.prototype.call),
25+
proxify = (value, construct, getters) =>
26+
new Proxy(value, {
27+
__proto__: null,
28+
get(_, key, receiver) {
29+
const getter = getters(key)
30+
return getter ? call(getter, receiver) : value[key]
31+
},
32+
construct,
33+
})
34+
proxify
35+
}

src/worker.js

Lines changed: 109 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,71 @@
11
const { worker } = require('workerpool')
22
const { VM } = require('vm2')
3-
const { inspect } = require('util')
3+
const {
4+
inspect,
5+
types: { isUint8Array },
6+
} = require('node:util')
47
const { Console } = console
5-
const { Writable } = require('stream')
8+
const { Writable } = require('node:stream')
9+
const { isEncoding, isBuffer } = Buffer
10+
const bufferToString = Function.prototype.call.bind(Buffer.prototype.toString)
11+
const vmSetupScript = require('node:fs').readFileSync(
12+
require('node:path').join(__dirname, './setup.js'),
13+
'utf8'
14+
)
615

716
const errorToString = err => {
8-
if (typeof err === 'object' && err instanceof Error) {
17+
if (typeof err === 'object' && err instanceof Error)
918
return Error.prototype.toString.call(err)
10-
}
1119
return 'Thrown: ' + inspect(err, { depth: null, maxArrayLength: null })
1220
}
1321

1422
const run = async code => {
1523
const consoleOutput = []
1624
const outStream = Object.defineProperty(new Writable(), 'write', {
17-
value: chunk => consoleOutput.push(chunk),
18-
})
19-
const vm = new VM({
20-
sandbox: {
21-
Set,
22-
Map,
23-
Date,
24-
WeakSet,
25-
WeakMap,
26-
Buffer,
27-
ArrayBuffer,
28-
SharedArrayBuffer,
29-
Int8Array,
30-
Uint8Array,
31-
Uint8ClampedArray,
32-
Int16Array,
33-
Uint16Array,
34-
Int32Array,
35-
Uint32Array,
36-
Float32Array,
37-
Float64Array,
38-
BigInt64Array,
39-
BigUint64Array,
40-
Atomics,
41-
DataView,
42-
console: new Console({
43-
stdout: outStream,
44-
stderr: outStream,
45-
inspectOptions: { depth: null, maxArrayLength: null },
46-
}),
25+
value(chunk, encoding, callback) {
26+
switch (typeof encoding) {
27+
case 'function':
28+
callback = encoding
29+
case 'undefined':
30+
encoding = 'utf8'
31+
break
32+
default:
33+
if (!isEncoding(encoding))
34+
throw new TypeError(`Unknown encoding '${encoding}'`)
35+
}
36+
switch (true) {
37+
case isUint8Array(chunk):
38+
chunk = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)
39+
case isBuffer(chunk):
40+
chunk = bufferToString(chunk, encoding)
41+
case typeof chunk === 'string':
42+
break
43+
default:
44+
throw new TypeError(
45+
'Invalid chunk type. The chunk must be a string, a Buffer or a Uint8Array'
46+
)
47+
}
48+
49+
consoleOutput.push(chunk)
50+
51+
if (typeof callback === 'function') callback()
4752
},
4853
})
54+
const vm = new VM()
4955

50-
const { call } = Function.prototype
56+
vm.freeze(Atomics, 'Atomics')
57+
vm.freeze(
58+
new Console({
59+
stdout: outStream,
60+
stderr: outStream,
61+
inspectOptions: { depth: null, maxArrayLength: null },
62+
}),
63+
'console'
64+
)
5165

5266
for (const type of ['Number', 'String', 'Boolean', 'Symbol', 'BigInt']) {
53-
const { prototype } = vm.run(type)
54-
const valueOf = call.bind(prototype.valueOf)
67+
const prototype = vm.run(type + '.prototype')
68+
const valueOf = Function.prototype.call.bind(prototype.valueOf)
5569
Object.defineProperty(prototype, inspect.custom, {
5670
value() {
5771
try {
@@ -62,28 +76,69 @@ const run = async code => {
6276
})
6377
}
6478

65-
const vmRegExpPrototype = vm.run('RegExp').prototype,
66-
vmRegExpProtoToString = call.bind(vmRegExpPrototype.toString)
67-
Object.defineProperty(vmRegExpPrototype, inspect.custom, {
68-
value() {
69-
try {
70-
return vmRegExpProtoToString(this)
71-
} catch {}
72-
return this
73-
},
74-
})
79+
const proxify = vm.run(vmSetupScript)
80+
81+
for (const constructor of [
82+
Set,
83+
Map,
84+
WeakSet,
85+
WeakMap,
86+
Buffer,
87+
ArrayBuffer,
88+
SharedArrayBuffer,
89+
Int8Array,
90+
Uint8Array,
91+
Uint8ClampedArray,
92+
Int16Array,
93+
Uint16Array,
94+
Int32Array,
95+
Uint32Array,
96+
Float32Array,
97+
Float64Array,
98+
BigInt64Array,
99+
BigUint64Array,
100+
DataView,
101+
]) {
102+
// Mark the class constructor readonly
103+
vm.readonly(constructor)
104+
105+
// Mark the prototype and its properties readonly
106+
for (let o = constructor.prototype; o; o = Object.getPrototypeOf(o)) {
107+
vm.readonly(o)
108+
Reflect.ownKeys(o).forEach(k => {
109+
try {
110+
vm.readonly(o[k])
111+
} catch {
112+
// Ignore errors
113+
}
114+
})
115+
}
116+
117+
// Add prototype mapping
118+
vm._addProtoMapping(constructor.prototype, constructor.prototype)
119+
120+
// Set class constructor to global
121+
vm.sandbox[constructor.name] = constructor.prototype.constructor = proxify(
122+
constructor,
123+
(_, args, newTarget) => Reflect.construct(constructor, args, newTarget),
124+
key => {
125+
for (let o = constructor; o; o = Reflect.getPrototypeOf(o)) {
126+
const getter = Reflect.getOwnPropertyDescriptor(o, key)?.get
127+
if (getter) return getter
128+
}
129+
}
130+
)
131+
}
75132

76-
let result
77133
try {
78-
result = await vm.run(code)
134+
const result = await vm.run(code)
135+
return [
136+
consoleOutput.join('').trim(),
137+
inspect(result, { depth: null, maxArrayLength: null }),
138+
]
79139
} catch (ex) {
80140
return [consoleOutput.join('').trim(), errorToString(ex)]
81141
}
82-
83-
return [
84-
consoleOutput.join('').trim(),
85-
inspect(result, { depth: null, maxArrayLength: null }),
86-
]
87142
}
88143

89144
worker({ run })

0 commit comments

Comments
 (0)