Prevent SQL Injection in Node.js Applications

SQL injection (SQLi) remains one of the most critical vulnerabilities in web development, allowing attackers to manipulate database queries to access, modify, or delete sensitive data. In Node.js applications, this risk typically arises when untrusted user input is directly concatenated into database queries. This article outlines the essential security best practices to protect your Node.js application from SQL injection, focusing on parameterized queries, database abstraction layers, rigorous input validation, and proper database permission management.

Use Parameterized Queries (Prepared Statements)

The most effective way to prevent SQL injection is by using parameterized queries (also known as prepared statements). Parameterization ensures that the database driver treats user input strictly as data, never as executable code.

Most popular Node.js database clients, such as pg (PostgreSQL) or mysql2 (MySQL), support parameterized queries natively.

Vulnerable Code (Do Not Use):

// Vulnerable to SQL injection via string interpolation
const query = `SELECT * FROM users WHERE email = '${req.body.email}' AND password = '${req.body.password}'`;
db.query(query);

Secure Code (Using Parameterized Queries):

// Secure: Input is treated strictly as parameters
const query = 'SELECT * FROM users WHERE email = $1 AND password = $2';
const values = [req.body.email, req.body.password];
db.query(query, values);

Utilize ORMs or Query Builders

Object-Relational Mapping (ORM) tools and query builders abstract the process of writing raw SQL. Libraries like Prisma, Sequelize, and Knex.js automatically use parameterized queries under the hood for almost all operations.

Example using Prisma:

// Prisma automatically parameterizes inputs
const user = await prisma.user.findUnique({
  where: {
    email: req.body.email,
  },
});

While ORMs greatly reduce the risk of SQL injection, be cautious when using raw query escape hatches (such as prisma.$queryRaw or sequelize.query) within these libraries, as they can still be vulnerable if implemented incorrectly.

Validate and Sanitize User Input

Never trust data coming from the client. Implement strict input validation and sanitization as a defense-in-depth measure before data ever reaches your database layer.

Use validation libraries like Joi, Zod, or express-validator to enforce strict schemas for incoming request data, ensuring that variables match expected data types, lengths, and formats.

Example using Joi:

const schema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required()
});

const { error, value } = schema.validate(req.body);
if (error) {
  return res.status(400).send('Invalid input data');
}

Limit Database Permissions (Least Privilege)

Secure your database at the infrastructure level by applying the principle of least privilege. The database user account used by your Node.js application should only have the minimum permissions necessary to function.

Avoid Dynamic Query Construction

Avoid dynamically constructing SQL query structures (such as table names, column names, or sort orders) based on user input. Parameterized queries only work for values, not for identifiers like table or column names.

If you must allow users to select columns or sort order, validate the inputs against a strict whitelist of allowed identifiers.

Secure Whitelisting Example:

const allowedSortColumns = ['username', 'created_at', 'email'];
const sortBy = allowedSortColumns.includes(req.query.sort) ? req.query.sort : 'created_at';

// Safe to concatenate because the variable is validated against a strict whitelist
const query = `SELECT id, username FROM users ORDER BY ${sortBy} ASC`;