Description
Code injection occurs when user input is evaluated as code or supplied as source for a template engine to execute. Examples include eval, new Function, string-based browser timers and dynamic template execution in engines such as EJS, Pug and Lodash. The effect depends on the engine's features and execution privileges: it may lead to remote code execution (RCE) on a server or script execution (XSS) in a browser. Node.js timers reject string callbacks instead of executing them.
Potential impact
- Injected server-side JavaScript may execute commands, access files or scan internal networks.
- Code runs with the application's permissions and may support further privilege-escalation attempts.
- Database content, environment variables or tokens may be exposed or modified.
- Scripts inserted into browser responses may compromise user sessions.
- Infinite loops or excessive memory use may disrupt the service.
Remediation
- Do not pass user input to code-execution APIs such as
eval,new Functionorvm.runIn*. - Compile and render only trusted, fixed template source. Pass user input as data, retain automatic escaping, and avoid unescaped output such as EJS
<%- %>for untrusted values. - Select predefined operations or functions from an allow-list when dynamic behavior is needed.
- Give timers callbacks that perform fixed operations. Do not evaluate input as code inside those callbacks.
- Validate input and encode output for its actual HTML, URL or JavaScript context.
- Do not use the Node.js
vmmodule as a security boundary for untrusted code.
Examples
Before
// Evaluate user input and render user-supplied template source
const express = require("express");
const ejs = require("ejs");
const app = express();
app.use(express.json());
app.get("/calc", (req, res) => {
const expr = req.query.expr; // For example, "process.env" or malicious code
// BAD: Evaluate user input as code
const result = eval(expr);
res.send(String(result));
});
app.post("/preview", (req, res) => {
const userTpl = req.body.tpl; // Template source supplied by the user
// BAD: Render user-supplied template source (SSTI)
const html = ejs.render(userTpl, { name: req.body.name });
res.send(html);
});
app.get("/wait", (req, res) => {
// BAD: Evaluate user input inside the timer callback
setTimeout(() => eval(req.query.code), 10);
res.send("scheduled");
});
app.listen(3000);
After
// Allowed operations, fixed template source and a fixed timer callback
const express = require("express");
const ejs = require("ejs");
const app = express();
app.use(express.json());
// 1) Select an allowed calculation
const OPS = new Map([
["add", (a, b) => a + b],
["sub", (a, b) => a - b],
["mul", (a, b) => a * b],
]);
app.get("/calc", (req, res) => {
const op = req.query.op; // one of add/sub/mul
const a = Number(req.query.a);
const b = Number(req.query.b);
const operation = typeof op === "string" ? OPS.get(op) : undefined;
if (!operation || !Number.isFinite(a) || !Number.isFinite(b)) {
return res.status(400).send("invalid input");
}
const result = operation(a, b); // Select a function without evaluating code
res.send(String(result));
});
// 2) Pass data to a fixed template with automatic escaping
const PROFILE_TPL = "<h1>User</h1><p>Name: <%= name %></p>";
app.post("/preview", (req, res) => {
const name = typeof req.body.name === "string" ? req.body.name : "";
const html = ejs.render(PROFILE_TPL, { name }); // Pass user input only as data
res.send(html);
});
// 3) Pass only a function callback to the timer
app.get("/wait", (req, res) => {
const ms = Math.min(Number(req.query.ms) || 0, 5000);
setTimeout(() => {
// Define the operation in a function; do not evaluate string code
}, ms);
res.send("scheduled");
});
app.listen(3000);
The first example can execute server code through eval and user-supplied templates. Its Node.js timer illustrates the same risk by calling eval inside a callback, rather than passing a string callback.
The second selects an allowed operation from a Map and accepts finite numbers. It separates the fixed EJS template from data and uses a fixed timer callback. Apply operation-specific value limits and request limits separately.