NodeJSPreventing SQL/NoSQL Injection

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

JS
// ❌ 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; --
String interpolation in SQL is a critical vulnerability — always use parameterized queries
SQL injection has been the #1 web vulnerability for over two decades. The rule is absolute: **never** build a SQL query by concatenating or interpolating user-supplied values into the string. It doesn't matter how you sanitize the input — a missed edge case, an encoding issue, or a multi-byte character can bypass string-level defences. Parameterized queries separate code from data at the protocol level; there is no escape sequence that can turn data back into SQL.
The fix: parameterized queries

JS
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.
Parameterized queries work at the protocol level — values are never parsed as SQL
When you use placeholders (`$1`, `?`), the driver sends the query template and the parameter array to the database **separately** over the wire. The database compiles the query plan first, then substitutes the values as data — they can't be interpreted as SQL syntax no matter what characters they contain. This is categorically different from string-escaping, which tries to neutralise dangerous characters before putting them into the SQL string and can always be subverted.
ORM safety — and where it can still fail

JS
// 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 })
ORMs don't protect you from injection in raw queries or template-literal WHERE clauses
Prisma, Sequelize, and TypeORM are safe when you use their query builder APIs — they parameterize automatically. But every ORM also exposes an escape hatch for raw SQL (e.g. `prisma.$queryRaw`, `sequelize.query`, TypeORM's query builder). If you use those with string interpolation, all injection protections are gone. The [raw SQL APIs use tagged templates](/nodejs/prisma) in Prisma precisely to force parameterization; in Sequelize and TypeORM, always pass a replacements/params object.
NoSQL injection — MongoDB

JS
// ❌ 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 strings
NoSQL injection bypasses auth by injecting MongoDB operators like `$gt`, `$ne`, `$regex`
MongoDB queries are JavaScript objects, and if a query field contains a MongoDB operator object (`{ $gt: '' }`, `{ $regex: '.*' }`, `{ $ne: null }`) instead of a plain value, the database evaluates the operator — not the literal string. The fix is twofold: **cast fields to the expected primitive types** before querying (coercing to string kills operator objects), and **validate with a schema** so an unexpected object shape is rejected before it reaches the database layer.
Mongoose and query sanitization

JS
// 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-sanitize as a backstop for Mongoose apps.

Next
The other critical injection threat — into the browser: [XSS Prevention](/nodejs/xss-prevention).