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 use express-mongo-sanitize.
  • Stored XSS — escape on output, and sanitize rich HTML with a library like DOMPurify before 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

Example · javascript
// 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.

Example · js
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.

Example · js
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.

Example · js
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

  • Be the first to comment on this lesson.