flawopen.com/sql-injection/Javascript
SQL-инъекции в JavaScript (mysql2, pg, Prisma, Knex)
Шаблонная строка или + внутри pool.query() позволяют параметру запроса переписать ваш SQL и вернуть все строки (CWE-89). Безопасный вызов в mysql2, node-postgres, Prisma и Knex, почему ${} безопасен в одном методе Prisma и опасен в соседнем и как сортировать без orderByRaw.[1], [2], [3], [4], [5], [6]
Представьте сортировочный центр: маршрут печатают и фиксируют до прибытия посылок. Имя на наклейке занимает отведённое поле и не может добавить остановку в маршрут. SQL-параметры работают так же: mysql2 execute(sql, [userId]) и pg query(sql, [status]) связывают значения, а тегированный $queryRaw в Prisma связывает каждое место ${}. Если сначала собрать SQL шаблонной строкой или через + либо вызвать $queryRawUnsafe, текст клиента попадёт прямо в маршрут. У Knex orderByRaw та же проблема с сортировкой: сопоставьте запрошенный ключ фиксированному столбцу и вызовите orderBy.[1], [2], [3], [4], [5], [6]
Ключевые понятия и термины
Template literals in query()- Строку в обратных кавычках с
${userId}вычисляет JavaScript ещё до вызова драйвера, так что драйвер получает готовый SQL со значением внутри. Синтаксис похож на плейсхолдер, но ничего не привязывает.[7], [1], [6] mysql2 execute() vs query()execute(sql, values)использует подготовленный оператор на сервере.query(sql, values)экранирует значения на клиенте и подставляет их в маркеры?. Оба варианта безопасны, если значения лежат в массиве; ни один не поможет, если строка SQL собрана через+или шаблонную строку. В mysql2multipleStatementsпо умолчанию выключен.[3], [6], [9]pg query(text, values)- node-postgres передаёт
valuesотдельно отtext, поэтому$1заполняет PostgreSQL. Без значений pg использует простой протокол запросов, который также принимает несколько операторов через точку с запятой за один вызов.[2], [6] Prisma $queryRaw vs $queryRawUnsafe$queryRaw— тегированный шаблон: Prisma превращает каждый${}в привязанный параметр.$queryRawUnsafe(string)принимает готовую строку, поэтому тот же${}в обычной шаблонной строке вклеивается в SQL. Названия отличаются одним словом, а риск — противоположный.[4], [6]
Связи со стандартами
- Ссылка CWE для слабого места, описанного в этом руководстве: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Пошаговый сценарий атаки
Исходный код: Уязвимый vs Защищённый вариант
// users.js: каждый запрос ниже собирает SQL из данных запроса
// mysql2: шаблонная строка вычисляется до вызова драйвера
export async function getUser(pool, userId) {
const [rows] = await pool.query(`SELECT id, email FROM users WHERE id = ${userId}`);
return rows[0];
}
// pg: конкатенация и никакого массива значений для привязки
export async function findOrders(pg, status) {
const { rows } = await pg.query("SELECT id FROM orders WHERE status = '" + status + "'");
return rows;
}
// Prisma: вариант Unsafe принимает готовую строку
export function findByEmail(prisma, email) {
return prisma.$queryRawUnsafe(`SELECT id FROM "User" WHERE email = '${email}'`);
}
// Knex: orderByRaw вставляет значение из запроса как сырой SQL
export function listUsers(knex, sortColumn) {
return knex('users').select('id', 'email').orderByRaw(sortColumn);
}
// users.js: значения идут в массиве значений, столбцы — из белого списка
const SORTABLE = new Map([['id', 'id'], ['email', 'email'], ['created', 'created_at']]);
// mysql2: подготовленный оператор на сервере, значение привязано к ?
export async function getUser(pool, userId) {
const [rows] = await pool.execute('SELECT id, email FROM users WHERE id = ?', [userId]);
return rows[0];
}
// pg: PostgreSQL заполняет $1 из массива значений
export async function findOrders(pg, status) {
const { rows } = await pg.query('SELECT id FROM orders WHERE status = $1', [status]);
return rows;
}
// Prisma: тегированный шаблон, каждый ${} становится привязанным параметром
export function findByEmail(prisma, email) {
return prisma.$queryRaw`SELECT id FROM "User" WHERE email = ${email}`;
}
// Knex: orderBy берёт разрешённый столбец в кавычки как идентификатор
export function listUsers(knex, sortKey) {
const column = SORTABLE.get(sortKey) ?? 'id';
return knex('users').select('id', 'email').orderBy(column);
}
Чек-лист по защите системы для инженеров
- ✓Запретите
$queryRawUnsafe,$executeRawUnsafe,whereRaw/orderByRawс интерполяцией, а также шаблонные строки и+в первом аргументеquery()/execute()правилом ESLintno-restricted-syntaxили Semgrep в CI.[1], [3], [2], [4], [5], [6] - ✓Передавайте каждое значение в массиве значений драйвера (
?для mysql2,$1для pg) или как${}в тегированном$queryRawPrisma.[3], [2], [4], [6] - ✓Проверяйте ввод схемой (zod, Joi) в маршруте: ID становятся числами, неизвестные ключи отклоняются, массивы не принимаются там, где ожидается строка.[8], [6]
- ✓Пропускайте ключи сортировки через белый список
Mapи передавайте результат вorderBy()Knex илиorderByPrisma; никогда не передавайтеreq.queryв сырой ORDER BY.[5], [4], [6] - ✓Оставьте
multipleStatementsв mysql2 выключенным и подключайтесь пользователем БД без прав на DDL.[3], [9], [6]
Источники
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- node-postgres — Parameterized query
- mysql2 — Using prepared statements
- Prisma — Raw queries and SQL injection
- Knex.js — Raw parameter binding
- OWASP SQL Injection Prevention Cheat Sheet
- MDN — Template literals (JavaScript)
- Express 4 API — req.query
- MySQL C API — Multiple statement execution support