Preventing SQL/NoSQL Injection
Injection is consistently the highest-severity vulnerability class: an attacker's data is interpreted as code by the database engine. In SQL that means reading, modifying, or deleting arbitrary data; in NoSQL it means bypassing query logic by injecting query operators. Both have the same root cause — mixing untrusted data into a command — and the same fix: parameterized queries and type-checked input. This page covers SQL injection, NoSQL injection, and the ORM-level gotchas that still allow injection despite apparent safety.
SQL injection — how it works
// ❌ Interpolating user input directly into SQL:
const email = req.body.email // attacker sends: ' OR '1'='1
const query = `SELECT * FROM users WHERE email = '${email}'`
// Executes: SELECT * FROM users WHERE email = '' OR '1'='1'
// Returns ALL users!
// Worse:
// attacker sends: '; DROP TABLE users; --
// Executes: SELECT * FROM users WHERE email = ''; DROP TABLE users; --The fix: parameterized queries
import { pool } from './db/pool.js'
// pg — $1, $2, ... placeholders:
const { rows } = await pool.query(
'SELECT * FROM users WHERE email = $1 AND active = $2',
[req.body.email, true], // values are NEVER part of the SQL string
)
// mysql2 — ? placeholders:
const [rows] = await pool.execute(
'SELECT * FROM users WHERE email = ? AND active = ?',
[req.body.email, true],
)
// Both drivers send the SQL and values separately to the DB server —
// the DB parses the SQL template first, then substitutes values safely.ORM safety — and where it can still fail
// Prisma — safe, auto-parameterized:
await prisma.user.findMany({ where: { email: req.body.email } })
// Sequelize — safe:
await User.findAll({ where: { email: req.body.email } })
// ❌ Sequelize raw query without parameterization:
await sequelize.query(`SELECT * FROM users WHERE email = '${req.body.email}'`)
// ✅ Sequelize raw query with replacements:
await sequelize.query('SELECT * FROM users WHERE email = :email', {
replacements: { email: req.body.email },
})
// ❌ TypeORM query builder with string interpolation:
repo.createQueryBuilder('u').where(`u.email = '${req.body.email}'`)
// ✅ TypeORM — named parameter:
repo.createQueryBuilder('u').where('u.email = :email', { email: req.body.email })NoSQL injection — MongoDB
// ❌ Passing req.body directly to a MongoDB query — operator injection:
// Attacker sends: { "password": { "$gt": "" } }
User.findOne({ email: req.body.email, password: req.body.password })
// Mongo sees: { password: { $gt: "" } } → matches any non-empty password!
// ✅ Validate and cast types before querying:
const email = String(req.body.email)
const password = String(req.body.password) // cast to string — not a MongoDB operator
User.findOne({ email, password })
// ✅ Better — use a validation schema (Zod/Joi) that asserts the types first:
const { email, password } = loginSchema.parse(req.body) // throws if not stringsMongoose and query sanitization
// mongoose-sanitize or express-mongo-sanitize to strip $ and . from req.body:
import mongoSanitize from 'express-mongo-sanitize'
app.use(mongoSanitize()) // replaces { "$gt": "" } keys with sanitized versions
// Still: validate types in your schema — mongoSanitize is a backstop, not a substitute.Injection prevention — the rules
Always use parameterized queries / prepared statements — no exceptions for SQL.
Validate and cast types before passing to any database query.
Use ORM query builder APIs, not raw string-based queries, wherever possible.
Raw queries require explicit parameterization — never interpolate user input.
For MongoDB: cast to primitive types or use schema validation before querying.
Add
express-mongo-sanitizeas a backstop for Mongoose apps.