Input Sanitization & Injection Defense
Validation checks shape; sanitization and parameterization keep injected payloads from ever reaching an interpreter.
Validation asks "is this the right shape?" Sanitization and safe interfaces ask "can this input change the meaning of a command?" You need both, and the second is where breaches happen.
The layered defense
- SQL injection — never build queries with string concatenation. Use parameterized queries or an ORM's bindings. This is non-negotiable and defeats the entire attack class.
- NoSQL / query-operator injection — a JSON body of
{ "$gt": "" }can turn a Mongo lookup into "match everything." Reject or strip keys starting with$and., or useexpress-mongo-sanitize. - Stored XSS — escape on output, and sanitize rich HTML with a library like
DOMPurifybefore storing.
// Safe: the driver keeps data and code separate.
await db.query('SELECT * FROM users WHERE email = $1', [req.body.email]);
// Unsafe: never do this.
// await db.query(`SELECT * FROM users WHERE email = '${req.body.email}'`);Example
// A small, dependency-free guard against query-operator injection.
// Reject any object key that could be interpreted as a Mongo operator.
function assertNoOperators(value, path = '$') {
if (Array.isArray(value)) {
value.forEach((v, i) => assertNoOperators(v, `${path}[${i}]`));
} else if (value && typeof value === 'object') {
for (const key of Object.keys(value)) {
if (key.startsWith('$') || key.includes('.')) {
throw Object.assign(new Error(`Illegal key '${key}' at ${path}`), { status: 400 });
}
assertNoOperators(value[key], `${path}.${key}`);
}
}
}
export const sanitizeBody = (req, res, next) => {
assertNoOperators(req.body); // req.body is writable; req.query is NOT in v5
next();
};
// At the data layer, ALWAYS parameterize — this is the real defense.
export async function findUserByEmail(db, email) {
return db.query('SELECT id, email FROM users WHERE email = $1 LIMIT 1', [email]);
}
// app.use(express.json());
// app.post('/search', sanitizeBody, searchController);When to use it
- A CMS comment system sanitises HTML input with DOMPurify to allow safe bold/italic formatting while stripping script tags and onclick attributes.
- A search endpoint strips MongoDB query operators from req.body using mongo-sanitize to prevent NoSQL injection attacks.
- A developer uses xss-clean middleware globally to sanitise all incoming request data (body, query, params) in a single app.use() call.
More examples
mongo-sanitize against NoSQL injection
Registers express-mongo-sanitize to strip MongoDB operator keys from req.body, query, and params before any route handler runs.
const mongoSanitize = require('express-mongo-sanitize');
// Strips keys that start with '$' or contain '.'
app.use(mongoSanitize());
app.post('/login', async (req, res) => {
// req.body.email can no longer contain { $gt: '' } injection
const user = await User.findOne({ email: req.body.email });
res.json({ found: !!user });
});xss-clean middleware
Applies xss-clean globally to escape HTML characters in all incoming request data, preventing stored XSS across the entire app.
const xss = require('xss-clean');
app.use(xss()); // sanitise req.body, req.query, req.params
app.post('/comments', (req, res) => {
// req.body.text has HTML entities escaped
res.status(201).json({ text: req.body.text });
});Allowlist HTML with sanitize-html
Uses sanitize-html to allow a safe subset of HTML tags for rich text while stripping all other elements and attributes.
const sanitizeHtml = require('sanitize-html');
app.post('/posts', (req, res) => {
const cleanContent = sanitizeHtml(req.body.content, {
allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
allowedAttributes: { a: ['href', 'title'] },
});
res.status(201).json({ content: cleanContent });
});
Discussion