Implement vm.compileFunction and fix some node:vm tests (#18285)

This commit is contained in:
Zack Radisic
2025-03-20 19:08:07 -07:00
committed by GitHub
parent f1cd5abfaa
commit 9f68db4818
14 changed files with 1435 additions and 93 deletions

View File

@@ -630,6 +630,7 @@ namespace ERR {
JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, const WTF::String& expected_type, JSC::JSValue val_actual_value)
{
auto message = Message::ERR_INVALID_ARG_TYPE(throwScope, globalObject, arg_name, expected_type, val_actual_value);
RETURN_IF_EXCEPTION(throwScope, {});
throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, message));
return {};
}
@@ -641,6 +642,7 @@ JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalO
auto arg_name = jsString->view(globalObject);
RETURN_IF_EXCEPTION(throwScope, {});
auto message = Message::ERR_INVALID_ARG_TYPE(throwScope, globalObject, arg_name, expected_type, val_actual_value);
RETURN_IF_EXCEPTION(throwScope, {});
throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, message));
return {};
}
@@ -649,10 +651,13 @@ JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalO
JSC::EncodedJSValue INVALID_ARG_INSTANCE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, const WTF::String& expected_type, JSC::JSValue val_actual_value)
{
auto& vm = JSC::getVM(globalObject);
ASCIILiteral type = String(arg_name).contains('.') ? "property"_s : "argument"_s;
WTF::StringBuilder builder;
builder.append("The \""_s);
builder.append(arg_name);
builder.append("\" argument must be an instance of "_s);
builder.append("\" "_s);
builder.append(type);
builder.append(" must be an instance of "_s);
builder.append(expected_type);
builder.append(". Received "_s);
determineSpecificType(vm, globalObject, builder, val_actual_value);

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@
#include "headers-handwritten.h"
#include "BunClientData.h"
#include <JavaScriptCore/CallFrame.h>
#include <JavaScriptCore/Nodes.h>
namespace Bun {

View File

@@ -700,7 +700,6 @@ JSC::EncodedJSValue V::validateObject(JSC::ThrowScope& scope, JSC::JSGlobalObjec
template JSC::EncodedJSValue V::validateInteger<size_t>(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue value, JSC::JSValue name, JSC::JSValue min, JSC::JSValue max, size_t* out);
template JSC::EncodedJSValue V::validateInteger<ssize_t>(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue value, JSC::JSValue name, JSC::JSValue min, JSC::JSValue max, ssize_t* out);
template JSC::EncodedJSValue V::validateInteger<uint32_t>(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue value, JSC::JSValue name, JSC::JSValue min, JSC::JSValue max, uint32_t* out);
template JSC::EncodedJSValue V::validateInteger<int32_t>(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue value, ASCIILiteral name, JSC::JSValue min, JSC::JSValue max, int32_t* out);
template JSC::EncodedJSValue V::validateInteger<size_t>(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue value, ASCIILiteral name, JSC::JSValue min, JSC::JSValue max, size_t* out);
template JSC::EncodedJSValue V::validateInteger<ssize_t>(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue value, ASCIILiteral name, JSC::JSValue min, JSC::JSValue max, ssize_t* out);

View File

@@ -604,7 +604,7 @@ WTF::String Bun::formatStackTrace(
if (!sourceURLForFrame.isEmpty()) {
sb.append(sourceURLForFrame);
if (displayLine.zeroBasedInt() > 0) {
if (displayLine.zeroBasedInt() > 0 || displayColumn.zeroBasedInt() > 0) {
sb.append(':');
sb.append(displayLine.oneBasedInt());

View File

@@ -5,7 +5,7 @@ const vm = $cpp("NodeVM.cpp", "Bun::createNodeVMBinding");
const ObjectFreeze = Object.freeze;
const { createContext, isContext, Script, runInNewContext, runInThisContext } = vm;
const { createContext, isContext, Script, runInNewContext, runInThisContext, compileFunction } = vm;
function runInContext(code, context, options) {
return new Script(code, options).runInContext(context);
@@ -15,9 +15,6 @@ function createScript(code, options) {
return new Script(code, options);
}
function compileFunction() {
throwNotImplemented("node:vm compileFunction");
}
function measureMemory() {
throwNotImplemented("node:vm measureMemory");
}

View File

@@ -0,0 +1,35 @@
'use strict';
// Tests that vm.createScript and runInThisContext correctly handle errors
// thrown by option property getters.
// See https://github.com/nodejs/node/issues/12369.
const common = require('../common');
const assert = require('assert');
const execFile = require('child_process').execFile;
const scripts = [];
['filename', 'cachedData', 'produceCachedData', 'lineOffset', 'columnOffset']
.forEach((prop) => {
scripts.push(`vm.createScript('', {
get ${prop} () {
throw new Error('xyz');
}
})`);
});
['breakOnSigint', 'timeout', 'displayErrors']
.forEach((prop) => {
scripts.push(`vm.createScript('').runInThisContext({
get ${prop} () {
throw new Error('xyz');
}
})`);
});
scripts.forEach((script, i) => {
const node = process.execPath;
execFile(node, [ '-e', script ], common.mustCall((err, stdout, stderr) => {
assert(typeof Bun === 'undefined' ? stderr.includes('Error: xyz') : stderr.includes('error: xyz'), 'createScript crashes');
}));
});

View File

@@ -0,0 +1,123 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
require('../common');
const assert = require('assert');
const vm = require('vm');
const Script = vm.Script;
let script = new Script('"passed";');
// Run in a new empty context
let context = vm.createContext();
let result = script.runInContext(context);
assert.strictEqual(result, 'passed');
// Create a new pre-populated context
context = vm.createContext({ 'foo': 'bar', 'thing': 'lala' });
assert.strictEqual(context.foo, 'bar');
assert.strictEqual(context.thing, 'lala');
// Test updating context
script = new Script('foo = 3;');
result = script.runInContext(context);
assert.strictEqual(context.foo, 3);
assert.strictEqual(context.thing, 'lala');
// Issue GH-227:
assert.throws(() => {
vm.runInNewContext('', null, 'some.js');
}, {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError'
});
// Issue GH-1140:
// Test runInContext signature
let gh1140Exception;
try {
vm.runInContext('throw new Error()', context, 'expected-filename.js');
} catch (e) {
gh1140Exception = e;
assert.match(e.stack, /expected-filename/);
}
// This is outside of catch block to confirm catch block ran.
assert.strictEqual(gh1140Exception.toString(), 'Error');
const nonContextualObjectError = {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: /must be of type object/
};
const contextifiedObjectError = {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: /The "contextifiedObject" argument must be an vm\.Context/
};
let i = 0;
[
[undefined, nonContextualObjectError],
[null, nonContextualObjectError],
[0, nonContextualObjectError],
[0.0, nonContextualObjectError],
['', nonContextualObjectError],
[{}, contextifiedObjectError],
[[], contextifiedObjectError],
].forEach((e) => {
assert.throws(() => { script.runInContext(e[0]); }, e[1]);
assert.throws(() => { vm.runInContext('', e[0]); }, e[1]);
});
// Issue GH-693:
// Test RegExp as argument to assert.throws
script = vm.createScript('const assert = require(\'assert\'); assert.throws(' +
'function() { throw "hello world"; }, /hello/);',
'some.js');
script.runInNewContext({ require });
// Issue GH-7529
script = vm.createScript('delete b');
let ctx = {};
Object.defineProperty(ctx, 'b', { configurable: false });
ctx = vm.createContext(ctx);
assert.strictEqual(script.runInContext(ctx), false);
// Error on the first line of a module should have the correct line and column
// number.
{
let stack = null;
assert.throws(() => {
vm.runInContext(' throw new Error()', context, {
filename: 'expected-filename.js',
lineOffset: 32,
columnOffset: 123
});
}, (err) => {
stack = err.stack;
return /^ \^/m.test(stack) &&
typeof Bun === 'undefined' ? /expected-filename\.js:33:131/.test(stack) : /expected-filename\.js:32:139/.test(stack);
}, `stack not formatted as expected: ${stack}`);
}
// https://github.com/nodejs/node/issues/6158
ctx = new Proxy({}, {});
assert.strictEqual(typeof vm.runInNewContext('String', ctx), 'function');

View File

@@ -0,0 +1,46 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
require('../common');
const assert = require('assert');
const vm = require('vm');
for (const valToTest of [
'string', null, undefined, 8.9, Symbol('sym'), true,
]) {
assert.throws(() => {
vm.isContext(valToTest);
}, {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError'
});
}
assert.strictEqual(vm.isContext({}), false);
assert.strictEqual(vm.isContext([]), false);
assert.strictEqual(vm.isContext(vm.createContext()), true);
assert.strictEqual(vm.isContext(vm.createContext([])), true);
const sandbox = { foo: 'bar' };
vm.createContext(sandbox);
assert.strictEqual(vm.isContext(sandbox), true);

View File

@@ -0,0 +1,94 @@
'use strict';
require('../common');
const assert = require('assert');
const vm = require('vm');
const invalidArgType = {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE'
};
const outOfRange = {
name: 'RangeError',
code: 'ERR_OUT_OF_RANGE'
};
assert.throws(() => {
new vm.Script('void 0', 42);
}, invalidArgType);
[null, {}, [1], 'bad', true].forEach((value) => {
assert.throws(() => {
new vm.Script('void 0', { lineOffset: value });
}, invalidArgType);
assert.throws(() => {
new vm.Script('void 0', { columnOffset: value });
}, invalidArgType);
});
[0.1, 2 ** 32].forEach((value) => {
assert.throws(() => {
new vm.Script('void 0', { lineOffset: value });
}, outOfRange);
assert.throws(() => {
new vm.Script('void 0', { columnOffset: value });
}, outOfRange);
});
assert.throws(() => {
new vm.Script('void 0', { lineOffset: Number.MAX_SAFE_INTEGER });
}, outOfRange);
assert.throws(() => {
new vm.Script('void 0', { columnOffset: Number.MAX_SAFE_INTEGER });
}, outOfRange);
assert.throws(() => {
new vm.Script('void 0', { filename: 123 });
}, invalidArgType);
assert.throws(() => {
new vm.Script('void 0', { produceCachedData: 1 });
}, invalidArgType);
[[0], {}, true, 'bad', 42].forEach((value) => {
assert.throws(() => {
new vm.Script('void 0', { cachedData: value });
}, invalidArgType);
});
{
const script = new vm.Script('void 0');
const sandbox = vm.createContext();
function assertErrors(options, errCheck) {
assert.throws(() => {
script.runInThisContext(options);
}, errCheck);
assert.throws(() => {
script.runInContext(sandbox, options);
}, errCheck);
assert.throws(() => {
script.runInNewContext({}, options);
}, errCheck);
}
[/*null,*/ 'bad', 42].forEach((value) => {
assertErrors(value, invalidArgType);
});
// [{}, [1], 'bad', null].forEach((value) => {
// assertErrors({ timeout: value }, invalidArgType);
// });
// [-1, 0, NaN].forEach((value) => {
// assertErrors({ timeout: value }, outOfRange);
// });
// [{}, [1], 'bad', 1, null].forEach((value) => {
// assertErrors({ displayErrors: value }, invalidArgType);
// assertErrors({ breakOnSigint: value }, invalidArgType);
// });
}

View File

@@ -0,0 +1,105 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
// Flags: --expose-gc
const common = require('../common');
const assert = require('assert');
const vm = require('vm');
if (typeof globalThis.gc !== 'function')
assert.fail('Run this test with --expose-gc');
// Run a string
const result = vm.runInNewContext('\'passed\';');
assert.strictEqual(result, 'passed');
// Thrown error
assert.throws(() => {
vm.runInNewContext('throw new Error(\'test\');');
}, /^Error: test$/);
globalThis.hello = 5;
vm.runInNewContext('hello = 2');
assert.strictEqual(globalThis.hello, 5);
// Pass values in and out
globalThis.code = 'foo = 1;' +
'bar = 2;' +
'if (baz !== 3) throw new Error(\'test fail\');';
globalThis.foo = 2;
globalThis.obj = { foo: 0, baz: 3 };
/* eslint-disable no-unused-vars */
const baz = vm.runInNewContext(globalThis.code, globalThis.obj);
/* eslint-enable no-unused-vars */
assert.strictEqual(globalThis.obj.foo, 1);
assert.strictEqual(globalThis.obj.bar, 2);
assert.strictEqual(globalThis.foo, 2);
// Call a function by reference
function changeFoo() { globalThis.foo = 100; }
vm.runInNewContext('f()', { f: changeFoo });
assert.strictEqual(globalThis.foo, 100);
// Modify an object by reference
const f = { a: 1 };
vm.runInNewContext('f.a = 2', { f });
assert.strictEqual(f.a, 2);
// Use function in context without referencing context
const fn = vm.runInNewContext('(function() { obj.p = {}; })', { obj: {} });
globalThis.gc();
fn();
// Should not crash
const filename = 'test_file.vm';
for (const arg of [filename, { filename }]) {
// Verify that providing a custom filename works.
const code = 'throw new Error("foo");';
assert.throws(() => {
vm.runInNewContext(code, {}, arg);
}, (err) => {
const lines = err.stack.split('\n');
assert.strictEqual(lines[0].trim(), `${filename}:1`);
if (typeof Bun === 'undefined') {
assert.strictEqual(lines[1].trim(), code);
// Skip lines[2] and lines[3]. They're just a ^ and blank line.
assert.strictEqual(lines[4].trim(), 'Error: foo');
assert.strictEqual(lines[5].trim(), `at ${filename}:1:7`);
} else {
assert.strictEqual(lines[1].trim(), 'Error: foo');
assert.strictEqual(lines[2].trim(), `at ${filename}:1:16`);
}
// The rest of the stack is uninteresting.
return true;
});
}
common.allowGlobals(
globalThis.hello,
globalThis.code,
globalThis.foo,
globalThis.obj
);

View File

@@ -0,0 +1,20 @@
'use strict';
require('../common');
const assert = require('assert');
const vm = require('vm');
// https://github.com/nodejs/node/issues/10223
const ctx = vm.createContext();
// Define x with writable = false.
vm.runInContext('Object.defineProperty(this, "x", { value: 42 })', ctx);
assert.strictEqual(ctx.x, 42);
assert.strictEqual(vm.runInContext('x', ctx), 42);
vm.runInContext('x = 0', ctx); // Does not throw but x...
assert.strictEqual(vm.runInContext('x', ctx), 42); // ...should be unaltered.
assert.throws(() => vm.runInContext('"use strict"; x = 0', ctx),
typeof Bun === 'undefined' ? /Cannot assign to read only property 'x'/ : /Attempted to assign to readonly property\./);
assert.strictEqual(vm.runInContext('x', ctx), 42);

View File

@@ -0,0 +1,29 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const child_process = require('child_process');
const fixtures = require('../common/fixtures');
const wrong_script = fixtures.path('keys/rsa_cert.crt');
const p = typeof Bun === 'undefined' ? child_process.spawn(process.execPath, [
'-e',
'require(process.argv[1]);',
wrong_script,
]) : child_process.spawn(process.execPath, [wrong_script]);
p.stdout.on('data', common.mustNotCall());
let output = '';
p.stderr.on('data', (data) => output += data);
p.stderr.on('end', common.mustCall(() => {
if (typeof Bun === 'undefined') {
assert.match(output, /BEGIN CERT/);
assert.match(output, /^\s+\^/m);
assert.match(output, /Invalid left-hand side expression in prefix operation/);
} else {
assert.match(output, /Expected ";" but found "CERTIFICATE"/);
}
}));

View File

@@ -65,6 +65,177 @@ describe("Script", () => {
message: "Class constructor Script cannot be invoked without 'new'",
});
});
describe("compileFunction()", () => {
const vm = require("vm");
// Security tests
test("Template literal attack should not break out of sandbox", () => {
const before = globalThis.hacked;
try {
const result = vm.compileFunction("return `\n`; globalThis.hacked = true; //")();
expect(result).toBe("\n");
expect(globalThis.hacked).toBe(before);
} catch (e) {
// If it throws, that's also acceptable as long as it didn't modify globalThis
expect(globalThis.hacked).toBe(before);
}
});
test("Comment-based attack should not break out of sandbox", () => {
const before = globalThis.commentHacked;
try {
const result = vm.compileFunction("return 1; /* \n */ globalThis.commentHacked = true; //")();
expect(result).toBe(1);
expect(globalThis.commentHacked).toBe(before);
} catch (e) {
expect(globalThis.commentHacked).toBe(before);
}
});
test("Function constructor abuse should be contained", () => {
try {
const result = vm.compileFunction("return (function(){}).constructor('return process')();")();
// If it doesn't throw, it should at least not return the actual process object
expect(result).not.toBe(process);
} catch (e) {
// Throwing is also acceptable
expect(e).toBeTruthy();
}
});
test("Regex literal attack should not break out of sandbox", () => {
const before = globalThis.regexHacked;
try {
const result = vm.compileFunction("return /\n/; globalThis.regexHacked = true; //")();
expect(result instanceof RegExp).toBe(true);
expect(result.toString()).toBe("/\n/");
expect(globalThis.regexHacked).toBe(before);
} catch (e) {
expect(globalThis.regexHacked).toBe(before);
}
});
test("String escape sequence attack should not break out of sandbox", () => {
const before = globalThis.stringHacked;
try {
const result = vm.compileFunction("return '\\\n'; globalThis.stringHacked = true; //")();
expect(result).toBe("\n");
expect(globalThis.stringHacked).toBe(before);
} catch (e) {
expect(globalThis.stringHacked).toBe(before);
}
});
test("Arguments access attack should be contained", () => {
try {
const result = vm.compileFunction("return (function(){return arguments.callee.caller})();")();
// If it doesn't throw, it should at least not return a function
expect(typeof result !== "function").toBe(true);
} catch (e) {
// Throwing is also acceptable
expect(e).toBeTruthy();
}
});
test("With statement attack should not modify Object.prototype", () => {
const originalToString = Object.prototype.toString;
const before = globalThis.withHacked;
const parsingContext = vm.createContext({});
try {
vm.compileFunction(
"with(Object.prototype) { toString = function() { globalThis.withHacked = true; }; } return 'test';",
[],
{
parsingContext,
},
)();
// Check that Object.prototype.toString wasn't modified
expect(Object.prototype.toString).toBe(originalToString);
expect(globalThis.withHacked).toBe(before);
} catch (e) {
// If it throws, also check that nothing was modified
expect(Object.prototype.toString).toBe(originalToString);
expect(globalThis.withHacked).toBe(before);
} finally {
// Restore just in case
Object.prototype.toString = originalToString;
}
});
test("Eval attack should be contained", () => {
const before = globalThis.evalHacked;
const parsingContext = vm.createContext({});
try {
vm.compileFunction("return eval('globalThis.evalHacked = true;');", [], { parsingContext })();
expect(globalThis.evalHacked).toBe(before);
} catch (e) {
expect(globalThis.evalHacked).toBe(before);
}
});
// Additional tests for other potential vulnerabilities
test("Octal escape sequence attack should not break out", () => {
const before = globalThis.octalHacked;
try {
const result = vm.compileFunction("return '\\012'; globalThis.octalHacked = true; //")();
expect(result).toBe("\n");
expect(globalThis.octalHacked).toBe(before);
} catch (e) {
expect(globalThis.octalHacked).toBe(before);
}
});
test("Unicode escape sequence attack should not break out", () => {
const before = globalThis.unicodeHacked;
try {
const result = vm.compileFunction("return '\\u000A'; globalThis.unicodeHacked = true; //")();
expect(result).toBe("\n");
expect(globalThis.unicodeHacked).toBe(before);
} catch (e) {
expect(globalThis.unicodeHacked).toBe(before);
}
});
test("Attempted syntax error injection should be caught", () => {
expect(() => {
vm.compileFunction("});\n\n(function() {\nconsole.log(1);\n})();\n\n(function() {");
}).toThrow();
});
test("Attempted prototype pollution should be contained", () => {
const originalHasOwnProperty = Object.prototype.hasOwnProperty;
try {
vm.compileFunction("Object.prototype.polluted = true; return 'done';")();
expect(Object.prototype.polluted).toBeUndefined();
} catch (e) {
// Throwing is acceptable
} finally {
// Clean up just in case
delete Object.prototype.polluted;
Object.prototype.hasOwnProperty = originalHasOwnProperty;
}
});
test("Attempted global object access should be contained", () => {
try {
const result = vm.compileFunction("return this;")();
// The "this" inside the function should not be the global object
expect(result).not.toBe(globalThis);
} catch (e) {
// Throwing is also acceptable
expect(e).toBeTruthy();
}
});
});
});
type TestRunInContextArg =
@@ -468,11 +639,12 @@ throw new Error("hello");
}
expect(err!.stack!.replaceAll("\r\n", "\n").replaceAll(import.meta.path, "<this-url>")).toMatchInlineSnapshot(`
"Error: hello
at hellohello.js:2:16
at runInNewContext (unknown)
at <anonymous> (<this-url>:459:5)"
`);
"evalmachine.<anonymous>:2
Error: hello
at hellohello.js:2:16
at runInNewContext (unknown)
at <anonymous> (<this-url>:630:5)"
`);
});
test("can get sourceURL inside node:vm", () => {
@@ -491,14 +663,14 @@ hello();
);
expect(err.replaceAll("\r\n", "\n").replaceAll(import.meta.path, "<this-url>")).toMatchInlineSnapshot(`
"4 | return Bun.inspect(new Error("hello"));
^
error: hello
at hello (hellohello.js:4:24)
at hellohello.js:7:6
at <anonymous> (<this-url>:479:15)
"
`);
"4 | return Bun.inspect(new Error("hello"));
^
error: hello
at hello (hellohello.js:4:24)
at hellohello.js:7:6
at <anonymous> (<this-url>:651:15)
"
`);
});
test("eval sourceURL is correct", () => {
@@ -515,12 +687,12 @@ hello();
`,
);
expect(err.replaceAll("\r\n", "\n").replaceAll(import.meta.path, "<this-url>")).toMatchInlineSnapshot(`
"4 | return Bun.inspect(new Error("hello"));
^
error: hello
at hello (hellohello.js:4:24)
at eval (hellohello.js:7:6)
at <anonymous> (<this-url>:505:15)
"
`);
"4 | return Bun.inspect(new Error("hello"));
^
error: hello
at hello (hellohello.js:4:24)
at eval (hellohello.js:7:6)
at <anonymous> (<this-url>:677:15)
"
`);
});