Injection Beyond SQL: Command, NoSQL and Template Injection

Intermediate
12 min

Injection Beyond SQL: Command, NoSQL and Template Injection

The previous lesson covered SQL injection, but SQL is only one interpreter your application talks to. Shell commands, MongoDB queries, template engines, LDAP directories and even log files all parse text, and each one can be tricked in the same way: untrusted input is concatenated into a string that the interpreter then executes. OWASP groups all of these under "A03: Injection".

In this lesson you will recognise the shared root cause, see how command, NoSQL and template injection arise in Node.js code, and apply the fix that works for every interpreter: keep data and code separate.

The Shared Root Cause

Every injection bug has the same shape:

javascript
const instruction = "fixed part " + userInput + " more fixed part"; interpreter.run(instruction);

The interpreter cannot tell which characters came from the developer and which came from the user. The fix is never "escape harder"; it is to hand the interpreter the code and the data through separate channels: prepared statements for SQL, argument arrays for processes, typed values for database queries, and variables for templates.

OS Command Injection

Calling a shell with user-controlled text lets shell metacharacters (;, &&, |, backticks) append extra commands.

javascript
const { exec, execFile } = require("node:child_process"); // Vulnerable: the whole string is parsed by /bin/sh exec(`convert ${req.query.file} out.png`); // Safe: no shell, arguments cannot become commands execFile("convert", [req.query.file, "out.png"], callback);

execFile and spawn (without shell: true) pass arguments directly to the program. Combine that with an allowlist on the value itself, as the pingHost function in the sample does, and prefer a library over a subprocess when one exists (for image processing, sharp instead of ImageMagick's CLI).

NoSQL Injection

MongoDB queries are JSON objects, and express.json() will happily parse an object where you expected a string. A login handler like User.findOne({ username: req.body.username, password: req.body.password }) accepts a body such as { "username": "admin", "password": { "$ne": "" } }, and the $ne operator matches any password.

Defenses, in order of preference:

javascript
// Validate the shape before it reaches the query const { z } = require("zod"); const Login = z.object({ username: z.string().max(64), password: z.string().max(256) }); const body = Login.parse(req.body); // throws if password is an object // Belt and braces: reject $-operators inside filters globally mongoose.set("sanitizeFilter", true);

The same principle applies to $where and mapReduce, which evaluate JavaScript on the server: avoid them entirely.

Server-Side Template Injection

Template engines such as EJS, Pug and Handlebars compile template source into code. If user input becomes part of the template source rather than a variable, the user can run template directives, and in many engines that means arbitrary code on the server.

javascript
// Vulnerable: input is compiled as template code res.send(ejs.render(`<h1>Hello ${req.query.name}</h1>`)); // Safe: template is fixed, input is passed as data and auto-escaped res.render("hello", { name: req.query.name });

The rule is simple: template files come from disk and never contain request data; request data is passed as variables and escaped by the engine (<%= %> in EJS, not <%- %>).

Other Interpreters to Watch

| Interpreter | Injection vector | Defense | |---|---|---| | LDAP | Filter strings built from input | Escape with the LDAP filter rules or use a library that builds filters | | XPath / XML | Query strings, external entities | Parameterised XPath, disable external entities | | Log files | Newlines in user input forge log lines | Strip \r and \n, log as structured JSON | | eval, new Function, vm | Any user text | Never evaluate user input; use a parser instead |

Quick Quiz
Question 1 of 2

Why is `execFile("ls", ["-l", dir])` safer than `exec("ls -l " + dir)`?

Key Takeaways

  • All injection bugs share one cause: untrusted text concatenated into something an interpreter executes.
  • Use execFile or spawn with argument arrays and an allowlist; never build shell strings.
  • Validate request bodies as strings and enable sanitizeFilter so MongoDB operators cannot arrive from users.
  • Templates are fixed files; user input is passed as escaped variables, never compiled as template source.
  • Treat LDAP, XPath, logs and eval with the same separation of code and data.

Next lesson: Input Validation and Output Encoding — the two-sided discipline that stops injection before it starts.

Injection Beyond SQL: Command, NoSQL and Template Injection - Cyber Security | CodeYourCraft | CodeYourCraft