SQL injection

SQL injection

Description

SQL injection occurs when user input is concatenated into query text without parameterization. The database may interpret it as SQL syntax rather than a value, allowing changes to WHERE conditions, UNION queries, subqueries, or function calls. Attackers can combine quotes, OR, UNION, or comments such as -- to bypass authentication or read, modify, or delete data. Concatenating input in raw queries through sequelize.query creates this risk.

Potential impact

  • Authentication bypass through manipulated login or authorization queries
  • Exposure of sensitive data through injected unions or subqueries
  • Data modification, deletion, or loss through commands such as INSERT, UPDATE, DELETE, or DROP
  • Denial of service from expensive queries or delay functions such as SLEEP

Remediation

  • Pass values separately using sequelize.query's replacements or bind options instead of concatenating input into SQL. Sequelize escapes replacements and inserts them into query text; bind parameters are sent to the database separately from that text.
  • Value placeholders cannot represent identifiers such as table or column names. Select those from a server-defined allow-list.
  • Prefer structured options such as attributes and where with Model.findAll or findOne. Using an ORM does not make manually assembled raw SQL safe.
  • Validate lengths and formats too. Do not rely on manual escaping or use the MySQL-specific sqlstring package for PostgreSQL queries.

Examples

Before

javascript
const express = require("express");
const { Sequelize } = require("sequelize");
const app = express();

const sequelize = new Sequelize("postgres://user:pass@localhost:5432/appdb");

// Before: concatenate user input directly into raw SQL.
app.get("/items", async (req, res) => {
  const col = req.query.col; // Example: name
  const term = req.query.term; // Example: a'
  const sql = `SELECT ${col} FROM products WHERE name LIKE '%${term}%'`;
  try {
    const [rows] = await sequelize.query(sql);
    res.json(rows);
  } catch (e) {
    res.status(500).send("error");
  }
});

After

javascript
const express = require("express");
const { Sequelize, QueryTypes } = require("sequelize");
const app = express();

const sequelize = new Sequelize("postgres://user:pass@localhost:5432/appdb");

// After: use replacements for values and an allowlist for identifiers.
app.get("/items", async (req, res) => {
  const allowedCols = new Set(["name", "price", "created_at"]);
  const requested = String(req.query.col || "name");
  if (!allowedCols.has(requested)) {
    return res.status(400).send("invalid column");
  }

  const term = String(req.query.term || "");
  // Pass values separately instead of concatenating them into SQL.
  const sql = `SELECT ${requested} FROM products WHERE name ILIKE ?`;
  try {
    const rows = await sequelize.query(sql, {
      replacements: [`%${term}%`],
      type: QueryTypes.SELECT,
    });
    res.json(rows);
  } catch (e) {
    res.status(500).send("error");
  }
});

// Alternative: use structured query options in the ORM API.
// Product.findAll({ attributes: [requested], where: { name: { [Op.iLike]: `%${term}%` } } });

Explanation:

  • Before: Inserting term and col directly into SQL lets input change query conditions or structure.
  • After: Values use replacements, while column names come only from a fixed allow-list. Replacements and database binding work differently, but both let the application supply values separately from SQL text to the API.

References