How to prevent SQL injection in JavaScript
SQL injection is one of the most critical security vulnerabilities in web applications, allowing attackers to execute malicious SQL commands. As the creator of CoreUI with over 25 years of web development experience since 2000, I’ve implemented secure database access patterns in countless production applications. The fundamental defense against SQL injection is using parameterized queries and prepared statements instead of string concatenation. This ensures user input is always treated as data, never as executable SQL code.
Use parameterized queries with placeholders instead of string concatenation.
// ❌ VULNERABLE - Never do this
const query = `SELECT * FROM users WHERE email = '${userEmail}'`
// ✅ SECURE - Use parameterized queries
const query = 'SELECT * FROM users WHERE email = ?'
db.query(query, [userEmail])
The vulnerable code directly interpolates user input into the SQL query, allowing attackers to inject malicious SQL. The secure version uses a placeholder ? and passes user input as a separate parameter array. The database driver automatically escapes the input, preventing injection attacks.
Using Parameterized Queries with MySQL
Implement secure queries with the mysql2 package.
const mysql = require('mysql2/promise')
async function getUserByEmail(email) {
const connection = await mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'myapp'
})
const [rows] = await connection.execute(
'SELECT * FROM users WHERE email = ?',
[email]
)
await connection.end()
return rows[0]
}
The execute() method uses prepared statements automatically. The ? placeholder is replaced with the escaped email value. Even if the email contains SQL syntax like ' OR '1'='1, it’s treated as a literal string value, not executable code.
Using Parameterized Queries with PostgreSQL
Implement secure queries with the pg package.
const { Pool } = require('pg')
const pool = new Pool({
host: 'localhost',
user: 'postgres',
password: 'password',
database: 'myapp'
})
async function getUserById(userId) {
const result = await pool.query(
'SELECT * FROM users WHERE id = $1',
[userId]
)
return result.rows[0]
}
PostgreSQL uses numbered placeholders like $1, $2, etc. The pool.query() method handles parameter escaping automatically. This pattern prevents injection regardless of what malicious input is provided in userId.
Using Named Parameters with SQLite
Implement secure queries with named parameters in better-sqlite3.
const Database = require('better-sqlite3')
const db = new Database('myapp.db')
function getUserByUsername(username) {
const stmt = db.prepare('SELECT * FROM users WHERE username = ?')
const user = stmt.get(username)
return user
}
function createUser(username, email) {
const stmt = db.prepare('INSERT INTO users (username, email) VALUES (?, ?)')
const info = stmt.run(username, email)
return info.lastInsertRowid
}
The prepare() method creates a prepared statement with placeholders. The get() and run() methods execute the statement with parameters. This approach is both secure and performant since prepared statements can be reused.
Using Query Builders for Complex Queries
Use query builders like Knex.js for dynamic queries with automatic parameter handling.
const knex = require('knex')({
client: 'mysql2',
connection: {
host: 'localhost',
user: 'root',
password: 'password',
database: 'myapp'
}
})
async function searchUsers(searchTerm, role) {
const users = await knex('users')
.where('role', role)
.andWhere('name', 'like', `%${searchTerm}%`)
.select('id', 'name', 'email')
return users
}
Knex automatically parameterizes all values passed to where(), andWhere(), and other methods. The query builder constructs safe SQL with proper escaping. This eliminates the need to manually write parameterized queries while maintaining security.
Using ORM for Safe Database Access
Use an ORM like Sequelize for automatic query parameterization.
const { Sequelize, DataTypes } = require('sequelize')
const sequelize = new Sequelize('mysql://root:password@localhost:3306/myapp')
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING,
role: DataTypes.STRING
})
async function findUsersByRole(role) {
const users = await User.findAll({
where: {
role: role
}
})
return users
}
ORMs like Sequelize handle all SQL generation and parameterization internally. The findAll() method with a where clause automatically creates a parameterized query. This provides security by default while offering a high-level API.
Validating and Sanitizing Input
Add input validation as a defense-in-depth measure.
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
async function getUserByEmail(email) {
if (!validateEmail(email)) {
throw new Error('Invalid email format')
}
const [rows] = await connection.execute(
'SELECT * FROM users WHERE email = ?',
[email]
)
return rows[0]
}
Input validation provides an additional security layer. The validateEmail() function rejects malformed input before it reaches the database. However, validation alone is insufficient - always use parameterized queries as the primary defense against SQL injection.
Best Practice Note
This is the same secure database access pattern we use throughout CoreUI backend services to protect against SQL injection attacks. Never trust user input - always use parameterized queries or prepared statements regardless of input validation. Avoid building dynamic SQL with string concatenation or template literals. When using ORMs or query builders, review the generated SQL in development to ensure proper parameterization. For complex queries requiring dynamic table or column names, use allowlists to validate identifiers before query construction. Remember that parameterized queries only work for values - table and column names must be validated separately if they come from user input.



