← Усі статті

Три шаблони SQL-ін'єкцій у Node.js, які досі потрапляють у прод

Конкатенація, шаблонні рядки та «безпечна» змінна sql — чому рев'ю пропускає вразливості й як ловити їх eslint-plugin-pg.

Зміст

Коротко

SQL-ін'єкції в Node.js не зникли: TypeScript і рев'ю коду часто пропускають три звичні шаблони в node-postgres. На Dev.to показали, як правило eslint-plugin-pg ловить їх статично — просто в редакторі, без важкого статичного аналізу безпеки.

Що сталося

Сценарій знайомий: лінтер чистий, рев'юер схвалив, реліз пішов у прод. Через три місяці пентестер знаходить рядок на кшталт pool.query("SELECT * FROM orders WHERE user_id = " + req.query.userId). Параметризовані запити всі знають, OWASP давно позначає ін'єкції як критичний ризик — але вразливості все одно проскакують, бо кожен шаблон виглядає безобидно в одному рядку.

Автор виділяє три форми. Пряма конкатенація в першому аргументі .query(). Шаблонні рядки з ${userId} — синтаксис чистіший, ризик той самий. Присвоєння в змінну: const sql = "…" + category і окремо client.query(sql) — на виклику нічого не кидається в очі, а отруєння рядка сталося двома рядками вище.

Правило pg/no-unsafe-query заточене під pg, а не під «будь-який рядок біля SELECT». Воно спрацьовує лише на .query(), розуміє контракт $1 + масив значень і позначає змінну «забрудненою» в момент присвоєння. Обгортка sanitize() не рятує — правило не може довести, що вона еквівалентна параметризації.

Чому це важливо

Універсальні лінтери або шумлять на безобидну збірку рядків, або пропускають шаблонні літерали. ESLint у межах однієї функції не відстежує «забруднення» через увесь проєкт, зате працює на кожному збереженні й у хуку перед комітом — там, де розробник реально править SQL.

Команди на Prisma, Knex або Drizzle теж не застраховані: $queryRaw, knex.raw і аналоги повертають ті самі три шаблони. Для сервісів на чистому pg — внутрішні API, пайплайни, мікросервіси — поверхня атаки саме тут.

На практиці

  1. Підключіть eslint-plugin-pg і ввімкніть pg/no-unsafe-query як помилку збірки.
  2. Скрізь, де введення користувача потрапляє в SQL, використовуйте $1, $2 і другий аргумент-масив — не конкатенацію й не ${} без плейсхолдерів $N.
  3. Не «рефакторте» параметризований запит у «читабельний» рядок — типовий шлях до регресії.
  4. Для імен схем/таблиць — pg-format або окремий шар, а не + SCHEMA_NAME (відомий хибнопозитивний випадок правила).
  5. Semgrep/CodeQL залиште для міжпроцедурного аналізу; ESLint — для миттєвого зворотного зв'язку в редакторі.

Підсумок

SQL-ін'єкції в Node.js — не проблема «невігласних джунів», а проблема шаблонів, які обманюють рев'ю. Спеціалізоване ESLint-правило для pg закриває три реальні форми; матеріал на Dev.to варто прочитати, якщо у вашому коді досі живе query("…" + id).