SQL Injection Prevention in Node.js - Complete Security Guide

Published: September 25, 2024 | Reading time: 12 minutes

⚠️ Critical Security Issue

SQL injection is one of the most dangerous web vulnerabilities. Here's the immediate fix:

❌ Vulnerable Code
// NEVER do this - vulnerable to SQL injection
const query = `SELECT * FROM users WHERE id = ${userId}`;
const result = await connection.query(query);
✅ Secure Code
// ALWAYS use parameterized queries
const query = 'SELECT * FROM users WHERE id = ?';
const result = await connection.execute(query, [userId]);

Understanding SQL Injection Attacks

SQL injection occurs when malicious SQL code is inserted into application queries, allowing attackers to:

  • Access unauthorized data
  • Modify or delete database records
  • Execute administrative operations
  • Compromise the entire database

How SQL Injection Works

Example Attack
// Vulnerable endpoint
app.get('/user/:id', async (req, res) => {
  const userId = req.params.id;
  const query = `SELECT * FROM users WHERE id = ${userId}`;
  // If userId = "1 OR 1=1", this becomes:
  // SELECT * FROM users WHERE id = 1 OR 1=1
  // This returns ALL users!
});

Prevention Methods

1. Parameterized Queries (Prepared Statements)

The most effective defense against SQL injection is using parameterized queries:

MySQL (mysql2)

MySQL Parameterized Queries
const mysql = require('mysql2/promise');

// ✅ Secure - Parameterized query
async function getUser(userId) {
  const query = 'SELECT * FROM users WHERE id = ?';
  const [rows] = await connection.execute(query, [userId]);
  return rows;
}

// ✅ Secure - Multiple parameters
async function createUser(name, email, age) {
  const query = 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)';
  const [result] = await connection.execute(query, [name, email, age]);
  return result;
}

// ✅ Secure - LIKE queries
async function searchUsers(searchTerm) {
  const query = 'SELECT * FROM users WHERE name LIKE ?';
  const [rows] = await connection.execute(query, [`%${searchTerm}%`]);
  return rows;
}

PostgreSQL (pg)

PostgreSQL Parameterized Queries
const { Pool } = require('pg');

// ✅ Secure - PostgreSQL uses $1, $2, etc.
async function getUser(userId) {
  const query = 'SELECT * FROM users WHERE id = $1';
  const result = await pool.query(query, [userId]);
  return result.rows;
}

// ✅ Secure - Multiple parameters
async function updateUser(id, name, email) {
  const query = 'UPDATE users SET name = $2, email = $3 WHERE id = $1';
  const result = await pool.query(query, [id, name, email]);
  return result;
}

// ✅ Secure - Complex queries
async function getUsersWithPagination(limit, offset) {
  const query = 'SELECT * FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2';
  const result = await pool.query(query, [limit, offset]);
  return result.rows;
}

2. Input Validation and Sanitization

Validate and sanitize all user inputs before database operations:

Input Validation with Joi
const Joi = require('joi');

// Define validation schemas
const userSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(18).max(120).required()
});

const idSchema = Joi.object({
  id: Joi.number().integer().positive().required()
});

// Validation middleware
function validateUser(req, res, next) {
  const { error, value } = userSchema.validate(req.body);
  if (error) {
    return res.status(400).json({ 
      error: 'Validation failed', 
      details: error.details 
    });
  }
  req.validatedData = value;
  next();
}

// Usage in route
app.post('/users', validateUser, async (req, res) => {
  try {
    const { name, email, age } = req.validatedData;
    const query = 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)';
    const [result] = await connection.execute(query, [name, email, age]);
    res.json({ id: result.insertId, message: 'User created' });
  } catch (error) {
    res.status(500).json({ error: 'Database error' });
  }
});

3. ORM Usage (Objection.js)

ORMs provide built-in protection against SQL injection:

Objection.js ORM
const { Model } = require('objection');
const Knex = require('knex');

// Configure Knex
const knex = Knex({
  client: 'mysql2',
  connection: {
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'mydb'
  }
});

Model.knex(knex);

// Define User model
class User extends Model {
  static get tableName() {
    return 'users';
  }
}

// ✅ Secure ORM queries
async function getUser(userId) {
  return await User.query().findById(userId);
}

async function createUser(userData) {
  return await User.query().insert(userData);
}

async function searchUsers(searchTerm) {
  return await User.query()
    .where('name', 'like', `%${searchTerm}%`);
}

async function updateUser(id, userData) {
  return await User.query()
    .findById(id)
    .patch(userData);
}

4. Database User Privileges

Limit database user permissions to minimize damage from potential attacks:

Secure Database User Setup
-- Create application user with minimal privileges
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';

-- Grant only necessary privileges
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'app_user'@'localhost';

-- Deny dangerous operations
REVOKE DROP, ALTER, CREATE ON *.* FROM 'app_user'@'localhost';

-- Flush privileges
FLUSH PRIVILEGES;

-- Example for PostgreSQL
CREATE USER app_user WITH PASSWORD 'strong_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
REVOKE ALL ON SCHEMA public FROM PUBLIC;

5. Advanced Security Measures

Query Monitoring and Logging

Query Logging Middleware
class SecureDatabaseService {
  constructor() {
    this.connection = mysql.createConnection({
      host: 'localhost',
      user: 'app_user',
      password: 'strong_password',
      database: 'mydb'
    });
  }

  async execute(query, params = []) {
    // Log all queries for monitoring
    console.log('Executing query:', query);
    console.log('With parameters:', params);
    
    // Check for suspicious patterns
    if (this.detectSuspiciousQuery(query)) {
      throw new Error('Suspicious query detected');
    }
    
    try {
      const [rows] = await this.connection.execute(query, params);
      return rows;
    } catch (error) {
      console.error('Database error:', error);
      throw error;
    }
  }

  detectSuspiciousQuery(query) {
    const suspiciousPatterns = [
      /union\s+select/i,
      /drop\s+table/i,
      /delete\s+from/i,
      /insert\s+into/i,
      /update\s+set/i,
      /--/,
      /\/\*/,
      /\*\//
    ];
    
    return suspiciousPatterns.some(pattern => pattern.test(query));
  }
}

Rate Limiting and Request Validation

Security Middleware
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP'
});

// Security headers
app.use(helmet());

// Custom security middleware
function securityMiddleware(req, res, next) {
  // Check for suspicious headers
  const userAgent = req.get('User-Agent') || '';
  if (userAgent.includes('sqlmap') || userAgent.includes('havij')) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  
  // Validate content type
  if (req.method === 'POST' && !req.is('application/json')) {
    return res.status(400).json({ error: 'Invalid content type' });
  }
  
  next();
}

app.use(limiter);
app.use(securityMiddleware);

Common Vulnerabilities to Avoid

1. String Concatenation

❌ Dangerous String Concatenation
// NEVER do this
const query = "SELECT * FROM users WHERE name = '" + userName + "'";
const query = `SELECT * FROM users WHERE name = '${userName}'`;
const query = "SELECT * FROM users WHERE name = '" + escape(userName) + "'";

2. Dynamic Query Building

❌ Dangerous Dynamic Queries
// NEVER build queries dynamically with user input
function buildQuery(filters) {
  let query = "SELECT * FROM users WHERE 1=1";
  
  if (filters.name) {
    query += ` AND name = '${filters.name}'`; // VULNERABLE!
  }
  
  if (filters.orderBy) {
    query += ` ORDER BY ${filters.orderBy}`; // VULNERABLE!
  }
  
  return query;
}
✅ Safe Dynamic Queries
// Safe dynamic query building
function buildSafeQuery(filters) {
  const conditions = [];
  const params = [];
  
  if (filters.name) {
    conditions.push('name = ?');
    params.push(filters.name);
  }
  
  if (filters.email) {
    conditions.push('email = ?');
    params.push(filters.email);
  }
  
  const whereClause = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
  
  // Safe order by with whitelist
  const allowedOrderBy = ['name', 'email', 'created_at'];
  const orderBy = allowedOrderBy.includes(filters.orderBy) ? filters.orderBy : 'created_at';
  
  const query = `SELECT * FROM users ${whereClause} ORDER BY ${orderBy}`;
  
  return { query, params };
}

Testing for SQL Injection

Manual Testing

Test Payloads
// Common test payloads
const testPayloads = [
  "' OR '1'='1",
  "' OR 1=1 --",
  "'; DROP TABLE users; --",
  "' UNION SELECT * FROM admin_users --",
  "1' OR '1'='1' --",
  "admin'--",
  "admin' /*",
  "' OR 'x'='x",
  "' AND (SELECT COUNT(*) FROM users) > 0 --"
];

// Test your endpoints with these payloads
async function testEndpoint(endpoint, payload) {
  try {
    const response = await fetch(`${endpoint}?id=${payload}`);
    const data = await response.json();
    
    // Check for unexpected results
    if (Array.isArray(data) && data.length > 1) {
      console.warn('Potential SQL injection vulnerability detected!');
    }
  } catch (error) {
    console.error('Test error:', error);
  }
}

Automated Testing

Security Testing with Jest
const request = require('supertest');
const app = require('../app');

describe('SQL Injection Tests', () => {
  const maliciousInputs = [
    "' OR '1'='1",
    "'; DROP TABLE users; --",
    "' UNION SELECT * FROM admin_users --"
  ];

  test.each(maliciousInputs)('should reject malicious input: %s', async (input) => {
    const response = await request(app)
      .get(`/user/${input}`)
      .expect(400);
    
    expect(response.body.error).toBeDefined();
  });

  test('should accept valid input', async () => {
    const response = await request(app)
      .get('/user/123')
      .expect(200);
    
    expect(response.body).toBeDefined();
  });
});

Security Tools and Libraries

express-rate-limit

Rate limiting middleware to prevent brute force attacks and abuse.

Install Rate Limiter

helmet

Security middleware that sets various HTTP headers to help protect your app.

Install Helmet

joi

Powerful schema validation library for JavaScript objects.

Install Joi

sqlmap

Automatic SQL injection and database takeover tool for testing.

Learn SQLMap

Summary

To prevent SQL injection in Node.js applications:

  • Always use parameterized queries - Never concatenate user input into SQL strings
  • Validate all inputs - Use libraries like Joi for input validation
  • Use ORMs when possible - They provide built-in protection
  • Limit database privileges - Use least privilege principle
  • Monitor and log queries - Detect suspicious activity
  • Implement rate limiting - Prevent brute force attacks
  • Test regularly - Use both manual and automated testing

Need Security Audit?

Our security experts can help you audit your Node.js application for SQL injection vulnerabilities and implement comprehensive security measures.

Get Security Help

🔗 Recommended Amazon Products

For Programming & Development

For Learning & Projects