XSS, SQL Injection, NoSQL Injection — Stopped At The Boundary
Input Sanitization
Validate, parameterize, and escape. Three habits that close most input-driven attack vectors.
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
- Validate at the boundary (Zod)
- Parameterize when constructing queries (
$1,?, etc.) - Escape when outputting (template engines, DOMPurify)
- Constrain when invoking external tools (execFile + arrays)
Apply these and you’ve eliminated the most common vulnerability classes.
Managing Secrets →