How do I fix SQL injection in Node.js?

Direct answer

Use parameterised queries. Pass user input as a separate parameter rather than building the SQL string yourself, so the database treats it as a value and never as code. Escaping or stripping characters is not a fix. It fails on edge cases and someone eventually finds one. Every major Node database client supports parameters, and using them costs nothing.

Muhammad HasanUpdated

SQL injection happens when user input becomes part of the query rather than a value inside it. In Node the usual shape is a template literal.

// Vulnerable
const user = await db.query(
  `SELECT * FROM users WHERE email = '${req.body.email}'`
);

Send ' OR '1'='1 as the email and the query returns every row. Send something worse and it does something worse.

The fix

Pass input as a parameter. The client sends the query and the values separately, so the database never parses the input as SQL.

node-postgres

const result = await db.query(
  'SELECT * FROM users WHERE email = $1',
  [req.body.email]
);

mysql2

const [rows] = await conn.execute(
  'SELECT * FROM users WHERE email = ?',
  [req.body.email]
);

That's the whole fix. It's not slower, and on most drivers it's marginally faster because prepared statements get reused.

Escaping is not the fix

The instinct is to clean the input, strip quotes, blocklist keywords, run it through an escape function. This keeps reappearing and it keeps failing.

Blocklists fail because SQL has more ways to express things than anyone enumerates. Escape functions fail when the encoding or character set differs from what the function assumed. And any approach based on cleaning input has to be applied correctly at every single call site, forever, by everyone who joins the team. Parameterisation is one habit and it either happened or it didn't.

Two cases parameters don't cover

Identifiers. You cannot parameterise a table or column name.

// Still vulnerable
db.query(`SELECT * FROM users ORDER BY ${req.query.sort}`);

Validate against an allowlist instead:

const allowed = { name: 'name', created: 'created_at' };
const column = allowed[req.query.sort] ?? 'created_at';
db.query(`SELECT * FROM users ORDER BY ${column}`);

Raw ORM queries. Prisma, Sequelize, Knex and TypeORM parameterise their query builders by default, which is why teams using them rarely see this bug. The escape hatch is where it comes back.

// Vulnerable — template literal in a raw query
await prisma.$queryRawUnsafe(
  `SELECT * FROM users WHERE email = '${email}'`
);

// Safe — tagged template parameterises
await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;

Search your codebase for $queryRawUnsafe, sequelize.query, knex.raw and similar. That's where it will be if it's anywhere.

What tooling will and won't catch

Better than most classes, and not as well as you'd hope. We measured this in RealVuln, our benchmark of 26 scanners across 66 Python repositories with hand-labelled vulnerabilities. On SQL injection specifically, rule-based scanners recalled 37% of what was there. LLM-based scanners recalled 96%.

So a clean report from a rule-based tool means roughly a one-in-three chance each instance was caught, not that your codebase is clean. The pattern is distinctive enough that these tools do markedly better here than on authorisation bugs, where the vulnerable code and the correct code are indistinguishable. But 37% is not coverage.

Grep alongside the scan. The call sites are few enough to read.

Go deeper

Fast remediation with Kolega

Related answers

See what your own repository returns

Connect a repo and run a scan. No credit card, no pipeline changes.

Get started for free