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.bodyorreq.query, including through object spread. - Keep template control keys, such as
layout,filename,views,partials,settings,helpersandruntimeOptions, 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
renderalso 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.