Code injection through eval or dynamic template compilation

Code injection through evaluated input or dynamic template source

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 Function or vm.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 vm module as a security boundary for untrusted code.

Examples

Before

javascript
// 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

javascript
// 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.

References