Input Sanitization

XSS, SQL Injection, NoSQL Injection — Stopped At The Boundary

Input Sanitization

Validate, parameterize, and escape. Three habits that close most input-driven attack vectors.

4 min read Level 2/5 #express#security#xss
What you'll learn
  • Avoid SQL injection
  • Escape HTML for output
  • Lock down NoSQL queries

User input is untrusted. The big three input-driven vulnerabilities have well-known mitigations.

SQL Injection

Never concatenate user input into SQL:

// 🚨 catastrophe
db.query(`SELECT * FROM users WHERE email = '${email}'`);

Always use parameterized queries:

// ✓
db.query("SELECT * FROM users WHERE email = $1", [email]);

Drizzle, Prisma, Knex, and every modern ORM does this for you.

NoSQL Injection

MongoDB is just as injectable when user input becomes a query object:

// 🚨 if req.body.password is { $ne: null }, this passes for any user
db.users.findOne({ email: req.body.email, password: req.body.password });

Always validate input types. Zod again:

const LoginSchema = z.object({
  email:    z.string().email(),
  password: z.string().min(8),
});

const { email, password } = LoginSchema.parse(req.body);
// now both are guaranteed strings — no operator injection

XSS — Output Encoding

If you render user-provided text into HTML, escape it. Template engines do this by default (<%= %> in EJS, {{ }} in Pug, React’s JSX).

The danger is when you bypass the escaping:

// EJS — <%- ... %> does NOT escape
<%- userBio %>

// React — dangerouslySetInnerHTML doesn't escape
<div dangerouslySetInnerHTML={{ __html: userBio }} />

If you absolutely need to render user-controlled HTML (a markdown preview, a rich-text bio), use a sanitizer:

npm install dompurify isomorphic-dompurify
import DOMPurify from "isomorphic-dompurify";

const safe = DOMPurify.sanitize(userBio, {
  ALLOWED_TAGS: ["b", "i", "u", "a", "p", "br"],
});

Path Traversal

When user input becomes a file path:

// 🚨 GET /files/../../etc/passwd reads outside your dir
res.sendFile(path.join("./uploads", req.params.name));

Resolve and check:

import path from "node:path";

const UPLOADS = path.resolve("./uploads");
const target  = path.resolve(UPLOADS, req.params.name);

if (!target.startsWith(UPLOADS + path.sep)) {
  return res.status(400).end();
}
res.sendFile(target);

Command Injection

Never pass user input to exec:

// 🚨 user input becomes a shell command
exec(`convert ${req.body.input} out.png`);

Use execFile with an args array:

execFile("convert", [req.body.input, "out.png"]);

Or validate req.body.input is a tight filename pattern first.

The Pattern

  1. Validate at the boundary (Zod)
  2. Parameterize when constructing queries ($1, ?, etc.)
  3. Escape when outputting (template engines, DOMPurify)
  4. Constrain when invoking external tools (execFile + arrays)

Apply these and you’ve eliminated the most common vulnerability classes.

Managing Secrets →