JavaScript Node.js SQL Injection Vulnerability

Critical Risk SQL Injection
javascriptnodejssql-injectiontemplate-literalsmysqldatabaseinjectionuser-inputparameterized-queriesserver-side

What it is

A critical vulnerability that occurs when Node.js applications using JavaScript construct SQL queries by concatenating or interpolating user input directly into query strings. Template literals and string concatenation in JavaScript make applications particularly vulnerable to SQL injection attacks when proper parameterization is not used.

const mysql = require('mysql');
const connection = mysql.createConnection({
  host: 'localhost',
  user: 'app_user',
  password: 'password',
  database: 'myapp'
});

function getUserByEmail(email) {
  // VULNERABLE: String concatenation
  const query = `SELECT * FROM users WHERE email = '\${email}'`;

  return new Promise((resolve, reject) => {
    connection.query(query, (error, results) => {
      if (error) reject(error);
      else resolve(results);
    });
  });
}

// Malicious input: getUserByEmail("'; DROP TABLE users; --")
const mysql = require('mysql');
const connection = mysql.createConnection({
  host: 'localhost',
  user: 'app_user',
  password: 'password',
  database: 'myapp'
});

function getUserByEmail(email) {
  // SECURE: Using parameterized query
  const query = 'SELECT * FROM users WHERE email = ?';

  return new Promise((resolve, reject) => {
    connection.query(query, [email], (error, results) => {
      if (error) reject(error);
      else resolve(results);
    });
  });
}

// Alternative: Using async/await with validation
async function getUserByEmailSafe(email) {
  // Input validation
  if (!email || typeof email !== 'string') {
    throw new Error('Valid email is required');
  }

  // Email format validation
  const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
  if (!emailRegex.test(email)) {
    throw new Error('Invalid email format');
  }

  const query = 'SELECT * FROM users WHERE email = ?';

  return new Promise((resolve, reject) => {
    connection.query(query, [email], (error, results) => {
      if (error) reject(error);
      else resolve(results);
    });
  });
}

💡 Why This Fix Works

Template literals and string concatenation make JavaScript particularly vulnerable to SQL injection. Using parameterized queries with the mysql library prevents this attack.

Why it happens

JavaScript template literals (backticks) provide convenient string interpolation but create serious SQL injection vulnerabilities when used to build database queries with user input. The ${} syntax performs direct substitution without any escaping.

Root causes

Template Literal SQL Injection

JavaScript template literals (backticks) provide convenient string interpolation but create serious SQL injection vulnerabilities when used to build database queries with user input. The ${} syntax performs direct substitution without any escaping.

Preview example – JAVASCRIPT
// VULNERABLE: Template literals in SQL queries
function getUserByEmail(email) {
  // Direct interpolation - SQL injection risk!
  const query = `SELECT * FROM users WHERE email = '${email}'`;
  return db.query(query);
}
// Attack: email = "'; DROP TABLE users; --"

String Concatenation with User Input

Using JavaScript string concatenation operators (+) to build SQL queries with user input bypasses all security mechanisms and allows direct injection of malicious SQL code.

Preview example – JAVASCRIPT
// VULNERABLE: String concatenation
function searchUsers(name, department) {
  const query = "SELECT * FROM users WHERE name LIKE '%" + name + 
                "%' AND department = '" + department + "'";
  return connection.query(query);
}
// Attack: name = "'; DELETE FROM users WHERE '1'='1'; --"

Dynamic Query Building in Node.js

Building SQL queries dynamically in Node.js applications without proper parameterization creates multiple injection points. This is especially common in Express.js applications that process user input from forms and URL parameters.

Preview example – JAVASCRIPT
// VULNERABLE: Dynamic query building
app.post('/update-user', (req, res) => {
  const { userId, status, notes } = req.body;
  
  // Multiple injection points
  const query = `UPDATE users SET status = '${status}', 
                 notes = '${notes}' WHERE id = ${userId}`;
  
  db.query(query, (err, result) => {
    res.json({ success: true });
  });
});
// Attack: status = "'; DROP TABLE users; --"

ORM Bypass with Raw Queries

Even when using ORMs like Sequelize or TypeORM, developers sometimes bypass the ORM's protections by using raw SQL queries with string interpolation, reintroducing SQL injection vulnerabilities.

Preview example – JAVASCRIPT
// VULNERABLE: Raw query bypassing ORM protections
const { QueryTypes } = require('sequelize');

async function getCustomReport(startDate, endDate, department) {
  // Bypassing Sequelize's parameterization
  const query = `
    SELECT COUNT(*) as total 
    FROM users 
    WHERE created_at BETWEEN '${startDate}' AND '${endDate}'
    AND department = '${department}'
  `;
  
  return sequelize.query(query, { type: QueryTypes.SELECT });
}
// Attack: department = "'; DROP TABLE users; --"

Fixes

1

Use Parameterized Queries with ? Placeholders

Always use parameterized queries with ? placeholders instead of string interpolation. Most Node.js database libraries (mysql, mysql2, pg) support parameterized queries that automatically escape user input.

View implementation – JAVASCRIPT
// SECURE: Parameterized queries
const mysql = require('mysql2');

function getUserByEmail(email) {
  // Input validation
  if (!email || typeof email !== 'string' || email.length > 100) {
    throw new Error('Invalid email format');
  }
  
  // Safe parameterized query
  const query = 'SELECT id, name, email FROM users WHERE email = ?';
  return new Promise((resolve, reject) => {
    connection.query(query, [email], (error, results) => {
      if (error) reject(error);
      else resolve(results);
    });
  });
}

// Using async/await
async function getUserByEmailAsync(email) {
  const query = 'SELECT id, name, email FROM users WHERE email = ?';
  const [rows] = await connection.execute(query, [email]);
  return rows;
}
2

Implement Comprehensive Input Validation

Validate all user input before using it in database operations. Use libraries like Joi or express-validator to implement robust validation schemas for your Node.js applications.

View implementation – JAVASCRIPT
// Input validation with Joi
const Joi = require('joi');

const userSchema = Joi.object({
  email: Joi.string().email().max(100).required(),
  name: Joi.string().max(50).required(),
  department: Joi.string().valid('IT', 'HR', 'Finance', 'Marketing').required(),
  userId: Joi.number().integer().positive().required()
});

function validateUserInput(data) {
  const { error, value } = userSchema.validate(data);
  if (error) {
    throw new Error(`Validation error: ${error.details[0].message}`);
  }
  return value;
}

// Safe Express.js route with validation
app.post('/update-user', async (req, res) => {
  try {
    const validatedData = validateUserInput(req.body);
    
    const query = 'UPDATE users SET status = ?, notes = ? WHERE id = ?';
    await connection.execute(query, [
      validatedData.status,
      validatedData.notes,
      validatedData.userId
    ]);
    
    res.json({ success: true });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});
3

Use ORM Query Builders Safely

Leverage ORMs like Sequelize, TypeORM, or Prisma that provide built-in protection against SQL injection. Always use the ORM's query methods rather than raw SQL when possible.

View implementation – JAVASCRIPT
// SECURE: Sequelize ORM usage
const { Op } = require('sequelize');

// Safe ORM queries
async function searchUsers(name, department) {
  // Input validation
  if (name && name.length > 100) {
    throw new Error('Search name too long');
  }
  
  const validDepartments = ['IT', 'HR', 'Finance', 'Marketing'];
  if (department && !validDepartments.includes(department)) {
    throw new Error('Invalid department');
  }
  
  // Safe Sequelize query
  return User.findAll({
    where: {
      name: {
        [Op.like]: `%${name}%`  // Sequelize handles escaping
      },
      department: department
    },
    limit: 100
  });
}

// Safe updates with Sequelize
async function updateUserStatus(userId, newStatus) {
  const user = await User.findByPk(userId);
  if (!user) {
    throw new Error('User not found');
  }
  
  return user.update({ status: newStatus });
}
4

Build Dynamic Queries Safely

When building dynamic queries, use arrays to separate SQL structure from user data. Build the WHERE clauses programmatically while maintaining parameterization.

View implementation – JAVASCRIPT
// SECURE: Safe dynamic query building
function buildUserQuery(filters) {
  const allowedFields = ['name', 'email', 'department', 'status'];
  const conditions = [];
  const params = [];
  
  let query = 'SELECT id, name, email, department FROM users WHERE 1=1';
  
  Object.entries(filters).forEach(([field, value]) => {
    if (!allowedFields.includes(field)) {
      throw new Error(`Invalid filter field: ${field}`);
    }
    
    if (value !== undefined && value !== null) {
      if (field === 'name') {
        conditions.push(' AND name LIKE ?');
        params.push(`%${value}%`);
      } else {
        conditions.push(` AND ${field} = ?`);
        params.push(value);
      }
    }
  });
  
  query += conditions.join('');
  query += ' LIMIT 100';
  
  return { query, params };
}

// Usage
async function searchUsersAdvanced(filters) {
  const { query, params } = buildUserQuery(filters);
  const [rows] = await connection.execute(query, params);
  return rows;
}
5

Use Prepared Statements for Performance

For frequently executed queries, use prepared statements which provide both security and performance benefits. Prepared statements are compiled once and parameterized for each execution.

View implementation – JAVASCRIPT
// SECURE: Prepared statements
class UserRepository {
  constructor(connection) {
    this.connection = connection;
    this.preparedStatements = new Map();
    this.initPreparedStatements();
  }
  
  async initPreparedStatements() {
    // Prepare frequently used queries
    const getUserByEmail = await this.connection.prepare(
      'SELECT id, name, email, department FROM users WHERE email = ?'
    );
    
    const updateUserStatus = await this.connection.prepare(
      'UPDATE users SET status = ?, updated_at = NOW() WHERE id = ?'
    );
    
    this.preparedStatements.set('getUserByEmail', getUserByEmail);
    this.preparedStatements.set('updateUserStatus', updateUserStatus);
  }
  
  async getUserByEmail(email) {
    const stmt = this.preparedStatements.get('getUserByEmail');
    const [rows] = await stmt.execute([email]);
    return rows[0];
  }
  
  async updateUserStatus(userId, status) {
    const validStatuses = ['active', 'inactive', 'suspended'];
    if (!validStatuses.includes(status)) {
      throw new Error('Invalid status');
    }
    
    const stmt = this.preparedStatements.get('updateUserStatus');
    const [result] = await stmt.execute([status, userId]);
    return result.affectedRows > 0;
  }
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies javascript node.js sql injection vulnerability and many other security issues in your codebase.