Template object injection

Template object injection

Description

Passing an entire user-controlled object, such as req.body or req.query, as template locals can let input affect rendering options. Depending on the engine, adapter and version, keys such as layout, filename, settings, partials or helpers may control file paths or template behavior. Manipulating these options can expose local files or, in some configurations, execute server-side code. Values such as {"layout":"../../../../etc/passwd"}, {"filename":"/etc/passwd"} or {"settings":{"views":"/"}} illustrate attempts to alter those controls.

Potential impact

  • Manipulated layout or include paths may expose local files.
  • Some engine and option combinations may permit server-side code execution.
  • Injected layouts, partials or helpers may alter pages or application behavior.
  • Changed settings may expose debugging information or support further attacks.

Remediation

  • Select only the fields the template needs and copy them into a new object. Do not pass all of req.body or req.query, including through object spread.
  • Keep template control keys, such as layout, filename, views, partials, settings, helpers and runtimeOptions, outside user control.
  • Validate the locals' types and value ranges with a schema, and reject unexpected fields.
  • Where supported, disable unnecessary code evaluation and restrict include paths to fixed directories.

Examples

The profile template and remaining view-engine installation and setup are omitted.

Before

javascript
const express = require("express");
const app = express();
app.set("view engine", "hbs");
app.use(express.json());

// Pass the entire user-controlled object as template locals
app.post("/profile", (req, res) => {
  // An attacker may inject control keys such as { "layout": "../../../../etc/passwd" }
  res.render("profile", req.body);
});

After

javascript
const express = require("express");
const app = express();
app.set("view engine", "hbs");
app.use(express.json());

// Copy only allowed keys into a new object
const SAFE_KEYS = ["name", "bio", "location"];
function pick(obj, keys) {
  const out = {};
  for (const k of keys) {
    if (typeof obj[k] === "string") out[k] = obj[k];
  }
  return out;
}

app.post("/profile", (req, res) => {
  const locals = pick(req.body || {}, SAFE_KEYS);
  res.render("profile", locals);
});

Explanation:

  • Before: Passing the whole request body to render also lets the caller supply keys that affect template behavior. The resulting file-access or code-execution risk depends on the engine and options.
  • After: Only required fields are copied into a new object, excluding caller-supplied control keys on this path. Validate field lengths and values and the template itself separately. Leave the object unfrozen so Express can add its rendering options.

References